useSelector
useSelector
is a Reflex Hook that lets you read and subscribe to a producer's state from your component.
const state = useSelector((state) => state.value);
Reference
useSelector(selector, isEqual?)
useSelector
subscribes to the root producer with the given selector
, and returns the selected state.
import { useSelector } from "@rbxts/roact-reflex";
import { selectTodos } from "./selectors";
function Todos() {
const todos = useSelector(selectTodos);
// ...
}
Parameters
selector
- A function that, given the root producer's state, returns a new value.isEqual
- An optional function that compares the previous and next values. If the function returnstrue
, the component will not re-render. By default,===
is used.
Returns
useSelector
returns the selected value from the root producer's state. If the value changes, the component will re-render.
Selectors that return new objects can cause excessive re-renders. If your selector performs array transformations or returns new objects, it should be memoized with createSelector
.
isEqual
function
isEqual
is a function that compares the current and previous values. If the function returns true
, the component will not re-render. By default, ===
is used.
import { useSelector } from "@rbxts/roact-reflex";
import { selectTodos } from "./selectors";
function Todos() {
const todos = useSelector(selectTodos, shallowEqual);
// ...
}
Parameters
current
- The current value of the selector.previous
- The previous value of the selector.
Returns
isEqual
returns true
if the current and previous values are equal.
Usage
Subscribing to a producer's state
Sometimes, components may want to access values from the producer's state. We'll go over how to create a basic todo list component that reads the producer's state. Don't forget to include <ReflexProvider>
in your app!
Call useSelector
from a function component to read a value from the producer's state:
import { useSelector } from "@rbxts/roact-reflex";
import { selectTodos } from "./selectors";
function Todos() {
const todos = useSelector(selectTodos);
// ...
}
Todos
will re-render whenever selectTodos
returns a new value. Functionally, useSelector
subscribes to the producer's state, and re-renders when the selected value changes.
You can then render a list of todos from the selected value:
function Todos() {
const todos = useSelector(selectTodos);
return (
<scrollingframe>
{todos.map((todo) => (
<Todo id={todo.id} />
))}
</scrollingframe>
);
}
Selectors that return new objects can cause excessive re-renders. State updates are compared by reference (===
), so if your selector creates a new object, Reflex will assume an update happened and re-render. Remember to memoize these selectors with createSelector
.
Custom equality comparison
By default, useSelector
uses ===
to compare the previous and next values. You can customize this behavior with the isEqual
parameter.
For example, some components might want to receive the latest state only if it's defined and exclude undefined
values. You can write an equality function that compares the current and previous values, and returns true
if the new value is undefined
:
import { useSelector } from "@rbxts/roact-reflex";
import { selectValue } from "./selectors";
function isEqualOrUndefined(current: unknown, previous: unknown) {
return current === previous || current === undefined;
}
function Button() {
const value = useSelector(selectValue, isEqualOrUndefined);
// ...
}
Remember that if the equality function returns true
, the component will not re-render for that state update.
The logic can be a bit difficult to follow, so let's break down the two cases:
current === previous
- If the current and previous values are equal, returntrue
. This is the default behavior.current === undefined
- If the current value isundefined
, returntrue
. This tells Reflex that the values are "equal," and thus the component will not re-render forundefined
values.
Using selector factories
Selectors can receive parameters other than state using selector factories, but using them with useSelector
can be unsafe.
Use factories with the useSelectorCreator
hook.
Using selectors with curried arguments
Selectors can receive parameters other than state using curried arguments. You can use these selectors with useSelector
by wrapping them in a function:
import { useSelector } from "@rbxts/roact-reflex";
import { selectTodo } from "./selectors";
function Todo({ id }: Props) {
const todo = useSelector((state) => selectTodo(state, id));
// ...
}
But you might wonder: why isn't this selector memoized? This is safe because useSelector
will only re-render when the selected value changes, and not necessarily when the selector function changes. It's safe to leave the selector function like this.
On the other hand, selector factories should be memoized, because creating a new selector with createSelector
on render also creates a new, empty argument cache, causing the selector to re-run when it shouldn't.
Troubleshooting
I'm getting an error: "useSelector
must be called from within a ReflexProvider
"
This error means that you're trying to use useSelector
in a function component that isn't wrapped in a <ReflexProvider>
, which throws this error because it uses useProducer
internally.
function TodosApp() {
const todos = useSelector(selectTodos);
// ...
}
// 🔴 Don't forget to wrap your root elements in a <ReflexProvider>
Roact.mount(<TodosApp />, container);
Roact Reflex uses Roact contexts to pass the producer to your components and allow them to use Reflex Hooks. If you don't wrap your root elements in a <ReflexProvider>
, your components won't be able to access the producer.
If your app or components use Reflex, you should wrap your root elements in a <ReflexProvider>
:
function TodosApp() {
const todos = useSelector(selectTodos);
// ...
}
// ✅ You can use the root producer in your components
Roact.mount(
<ReflexProvider producer={producer}>
<TodosApp />
</ReflexProvider>,
container,
);
My component is re-rendering too often
If your component is re-rendering too often, you might be using a selector that returns a new object every time it's called. Remember that Reflex uses reference equality (===
) to compare the previous and next values, so if your selector returns a new object, Reflex will assume an update happened and re-render.
Here's an example of a bad selector that returns a new object every time it's called:
const selectTodos = (state: RootState) => {
state.todos;
};
// 🔴 This selector creates a new array every time it's called
const selectTodosDone = (state: RootState) => {
return state.todos.filter((todo) => todo.done);
};
Because selectors are called every time the producer updates, and selectTodosDone
works by creating a new array, Reflex assumes that the value changed. This will cause the component to re-render with the new value, even if the underlying todos
state didn't change.
To fix this, you can use createSelector
to memoize your selectors and prevent unnecessary re-renders:
import { createSelector } from "@rbxts/roact-reflex";
const selectTodos = (state: RootState) => {
state.todos;
};
// ✅ This selector is memoized, and won't re-render unless 'todos' changes
const selectTodosDone = createSelector(selectTodos, (todos) => {
return todos.filter((todo) => todo.done);
});