Selectors and derived state
Subscribe to a slice, derive values, depend on local state, share selector hooks, and control equality.
Compose a selection. Reuse the hook. Subscribe to its result.
The problem this solves
A component that reads the whole state renders on every change, even when it shows one field. A selector narrows what the component depends on.
Select a slice
const [name] = useProfile((profile) => profile.name);After every store change the selector runs and the result is compared with the previous result using strict
equality (===). The component renders only when they differ. useProfile.select(selector) is the same
without the setter.
Selections that depend on local state
A selector often depends on something that is not in the store, such as a filter chosen in the component. List
those values in dependencies. When one changes, the selector runs again on the next render.
import { useState } from 'react';
import { createGlobalState, shallowCompare } from 'react-global-state-hooks';
export const useTodos = createGlobalState({
todos: [
{ id: 1, text: 'Write docs', done: true },
{ id: 2, text: 'Ship examples', done: false },
{ id: 3, text: 'Review', done: false },
],
});
export function TodoList() {
// Local state that is NOT in the store, but the selection depends on it.
const [showDone, setShowDone] = useState(false);
const [visible] = useTodos((state) => state.todos.filter((todo) => todo.done === showDone), {
// re-run the selector when this changes, not only when the store does
dependencies: [showDone],
isEqual: shallowCompare,
});
return (
<div>
<button onClick={() => setShowDone(!showDone)}>{showDone ? 'Show open' : 'Show done'}</button>
<ul>
{visible.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}dependencies can also be passed as the second argument directly: useTodos(selector, [showDone]).
Equality options
| Option | Default | Use it to |
|---|---|---|
isEqual(current, next) | === on the selected value | Treat a rebuilt array or object as unchanged. shallowCompare is exported for this. |
isEqualRoot(current, next) | === on the whole state | Skip running the selector at all for certain root changes. |
Share a derivation with selector hooks
store.createSelectorHook(selector) builds a hook once. It computes the derived value once per store change,
not once per component, and it chains. It returns the selected value directly, not a tuple.
import { createGlobalState, shallowCompare } from 'react-global-state-hooks';
export const useStore = createGlobalState({
users: [
{ id: 1, name: 'Ada', active: true },
{ id: 2, name: 'Grace', active: false },
],
currentUserId: 1,
});
// A selector hook is created once and shared. It returns the selected value, not a tuple.
export const useUsers = useStore.createSelectorHook((state) => state.users);
// Selector hooks chain. `isEqual` stops the derived list from changing when its items did not.
export const useActiveUsers = useUsers.createSelectorHook((users) => users.filter((user) => user.active), {
isEqual: shallowCompare,
});
export function ActiveUsers() {
const active = useActiveUsers();
return <p>{active.map((user) => user.name).join(', ')}</p>;
}Selector hooks are read-only: they have no setter. getState() and subscribe() work on them too.
Observe outside React
createObservable gives you the same slice as a subscribable object that needs no component. It calls back
immediately with the current value, then on each change.
import { createGlobalState } from 'react-global-state-hooks';
export const useCounter = createGlobalState({ count: 0, label: 'clicks' });
// An observable is a read-only, subscribable slice of the store. It needs no component.
export const count$ = useCounter.createObservable((state) => state.count);
export const seen: number[] = [];
// Runs immediately with the current value, then whenever `count` changes.
export const stop = count$.subscribe((count) => seen.push(count));Call dispose() on an observable or selector hook you no longer need, so it stops listening to its parent.