Selecting State
Similar to the subscribe
method, the useSelector
hook lets you select a value from the producer's state.
- 🍰 How to select a value from the producer
- ⚙️ How to select state with a selector factory
- 📦 How to write a typed
useSelector
hook
Subscribing to state
Let's say we had the same todos
slice from the previous guide in our root producer:
interface TodosState {
readonly list: readonly string[];
}
// ...
export const todosSlice = createProducer(initialState, {
addTodo: (state, todo: string) => ({
...state,
list: [...state.list, todo],
}),
});
If you want to select the todo list from the state in your component, you might try to subscribe to the producer and update the component's state whenever the todo list changes:
const selectTodos = (state: RootState) => state.todos.list;
function TodoList() {
const producer = useRootProducer();
const [todos, setTodos] = useState(producer.getState(selectTodos)));
useEffect(() => {
return producer.subscribe(selectTodos, setTodos);
}, [])
// ...
}
Now you have the todo list in your component's state, but you have to update it manually whenever the todo list changes. This is where the useSelector
hook comes in:
const selectTodos = (state: RootState) => state.todos.list;
function TodoList() {
const todos = useSelector(selectTodos);
// ...
}
The useSelector
hook automatically subscribes to the producer you passed to the <ReflexProvider>
and updates the component whenever the selected value changes!
The same rules apply to the useSelector
hook as the subscribe
method. If you pass a selector that creates a new object or array every time it's called, your component can re-render more often than you expect.
Passing arguments to selectors
Often, you'll want to select a value from the producer that depends on props passed to your component. For example, if you have a TodoList
component that takes a sortDirection
prop, you might want to select the todos that match the filter.
Sorting todos
First, let's write a selector factory that takes the sortDirection
argument:
const selectTodos = (state: RootState) => state.todos.list;
const selectSortedTodos = (sortDirection: "asc" | "desc") => {
return createSelector(selectTodos, (todos) => {
return [...todos].sort((a, b) => {
return sortDirection === "asc" ? a < b : a > b;
});
});
};
This selector factory creates a memoized selector, which will only recompute the value when the todos
dependency changes. It's good practice to memoize selectors that return new objects or arrays.
The wrong way to use a selector factory
To use this selector in your component, you might try to call the selector factory in your component on its own:
function TodoList({ sortDirection }: Props) {
// 🔴 Avoid: creates a selector every render, re-runs too often
const todos = useSelector(selectSortedTodos(sortDirection));
// ...
}
But calling selector factories like this is dangerous! It's not safe to create a selector inside of a component without passing it to useMemo
. This is because of how selector factories work when the selector is created with createSelector
.
When you call a selector factory, it creates a new memoized selector initialized with a new cache. This means that the selector will "forget" its previous values and recompute it from scratch. So, how do we use a selector factory in a component?
Using a selector factory
To use a selector factory, you need to avoid creating the selector unless the arguments change. You can do this with useMemo
:
function TodoList({ sortDirection }: Props) {
// ✅ Good: create a selector when the sortDirection changes
const selector = useMemo(() => {
return selectSortedTodos(sortDirection);
}, [sortDirection]);
const todos = useSelector(selector);
// ...
}
Now, the selector will only be created when the sortDirection
prop changes, allowing the selector to properly memoize its value.
This is also exactly what the useSelectorCreator
hook does!
function TodoList({ sortDirection }: Props) {
// ✅ Good: use the useSelectorCreator hook
const todos = useSelectorCreator(selectSortedTodos, sortDirection);
// ...
}
Because your factory is memoized by its arguments, you should avoid passing arguments that change every re-render. For example, if you pass a function as an argument, it could be re-created every render, causing extra re-renders:
function TodoList() {
// 🔴 Avoid: selector re-created for a new function every render
const todos = useSelectorCreator(selectSortedTodos, (a, b) => a < b);
// ...
}
Remember to memoize any arguments that change every render, including arrays and objects!
function TodoList() {
// ✅ Good: memoizes the argument
const sorter = useMemo(() => (a, b) => a < b, []);
const todos = useSelectorCreator(selectSortedTodos, sorter);
// ...
}
Typed useSelector
hook
You might want to create a selector and pass it to useSelector
manually:
function Button() {
const todos = useSelector((state: RootState) => state.todos.list);
// ...
}
But this can get repetitive if you have to write the same state
type for every selector. You can create a typed useSelector
hook to avoid this:
import { UseProducerHook, UseSelectorHook, useProducer, useSelector } from "@rbxts/roact-reflex";
import { RootProducer } from "./producer";
export const useRootProducer: UseProducerHook<RootProducer> = useProducer;
export const useRootSelector: UseSelectorHook<RootProducer> = useSelector;
Now, you can use the typed useSelector
hook in your components:
function Button() {
const todos = useRootSelector((state) => state.todos.list);
// ...
}
You shouldn't create a typed useSelectorCreator
hook because the state
types should be manually defined by the selector factory outside of the component.
Summary
- You can call
useSelector
to select a value from the producer. - Use the
useSelectorCreator
hook to create a memoized selector factory that takes arguments. - You can create a typed
useSelector
hook to avoid writing the samestate
type if you create selectors manually.