AtomSelector
A function that receives an AtomGetters object as its first parameter. It can take any number of other parameters and can return anything. It can use the AtomGetters to get atom values and run other AtomSelectors.
This is the most basic tool for deriving state in Zedux. The selectors walkthrough teaches the basics of AtomSelectors. This page will only cover a few things the walkthrough doesn't.
AtomSelectors define a blueprint for pulling data from atoms. Similar to reselect selectors, they don't do anything until someone calls them. You "call" them by passing them to various functions:
useAtomSelector()
injectAtomSelector()
ecosystem.select()
- The
select
AtomGetter function of other AtomSelectors (selector composition!)
You can also call them directly, passing an AtomGetters object. There is one situation where this can be necessary.
Use AtomSelectors for:
- Dynamically registering graph edges in components
- Getting part of an atom instance's state
- Simple calculations that don't need memoization
Definition
type AtomSelector<T = any, Args extends any[] = []> = (
getters: AtomGetters,
...args: Args
) => T
interface AtomSelectorConfig<T = any, Args extends any[] = []> {
argsComparator?: (newArgs: Args, oldArgs: Args) => boolean
name?: string
resultsComparator?: (newResult: T, oldResult: T) => boolean
selector: AtomSelector<T, Args>
}
type AtomSelectorOrConfig<T = any, Args extends any[] = []> =
| AtomSelector<T, Args>
| AtomSelectorConfig<T, Args>
Everywhere Zedux accepts an AtomSelector, it also accepts an AtomSelectorConfig object. AtomSelectorOrConfig
is the type you'll see for this.
Examples
AtomSelectors are extremely flexible. They can be used and composed in many ways.
// basic derivation
const finishedTodos = ({ get }) => get(todosAtom).filter(todo => todo.isDone)
const uninishedTodos = ({ get }) => get(todosAtom).filter(todo => !todo.isDone)
// composition using select()
const separatedTodos = ({ select }) => {
const finished = select(finishedTodos)
const unfinished = select(unfinishedTodos)
return { finished, unfinished }
}
// passing params
const getTodos = ({ select }, isDone) => {
const todos = select(isDone ? finishedTodos : unfinishedTodos)
}
const separatedTodosWithParams = ({ select }) => {
const finished = select(getTodos, true)
const unfinished = select(getTodos, false)
return { finished, unfinished }
}
// in useAtomSelector()
const withHook = useAtomSelector(finishedTodos)
// in injectAtomSelector()
const withInjector = injectAtomSelector(finishedTodos)
// in ecosystem.select()
const staticViaEcosystem = ecosystem.select(finishedTodos)
// directly in ion body (not recommended)
const exampleIon = ion('example', getters => {
const todos = finishedTodos(getters)
})
// only run selector once
const stateSnapshot = useAtomSelector({
// argsComparator isn't called on the first run
argsComparator: () => true,
selector: mySelector,
})
// only run the selector if the new args don't deep match the old args
const limitedRuns = useAtomSelector(
{
argsComparator: _.isEqual,
selector: mySelector,
},
arg1,
arg2
)
// only trigger updates if the new result doesn't deep match the old result
const limitedUpdates = useAtomSelector({
resultsComparator: _.isEqual,
selector: mySelector,
})
While passing an inline config object like this is fine, you should try to avoid it when possible.
Zedux caches selectors by reference. If the selector function or AtomSelectorConfig object reference change every evaluation/render, Zedux has to do extra work to reconfigure the dependency graph every time. While this is usually fine, be mindful of it in larger apps.
When Not to Use
AtomSelectors aren't atoms. As such, you can't use injectors in them. Ions (or other atoms) should be preferred when you need:
- To memoize anything - use an atom with
injectMemo()
- To create and update a new store - use an atom with
injectStore()
- To run side effects based on state updates - use an atom with
injectEffect()
- To manage promises or trigger React suspense - use
AtomApi#setPromise()
in an atom.
Converting AtomSelectors
AtomSelectors are simple and have little overhead, hence it's usually desirable to try an AtomSelector first for most tasks. However, sometimes you'll find out later that an AtomSelector needs functionality only atoms have.
Since AtomSelectors are so similar to ions, it's easy to convert an AtomSelector to an ion or a hook or injector.
// before:
const getSortedList = ({ get }: AtomGetters) => [...get(listAtom)].sort()
// after (as ion):
const sortedList = ion('sortedList', ({ get }) => {
const list = get(listAtom)
return injectMemo(() => [...list].sort(), [list]) // now we can memoize!
})
// after (as injector):
const injectSortedList = ({ get }: AtomGetters) => {
const list = get(listAtom)
return injectMemo(() => [...list].sort(), [list])
}
If refactoring the AtomSelector is too much work or too tedious or risky, you can also create an ion that simply wraps the existing AtomSelector:
// before
const getSortedList = ({ get }: AtomGetters) => [...get(listAtom)].sort()
// (before usage):
const sortedList = useAtomSelector(getSortedList)
// after
const getSortedListImpl = ({ get }: AtomGetters) => [...get(listAtom)].sort()
const getSortedList = ion('getSortedList', ({ select }) =>
select(getSortedListImpl)
)
// (after usage):
const sortedList = useAtomValue(getSortedList)
Generic AtomSelectors
Since Zedux calls your atom selector, there are some special considerations to take into account if your selector function takes generics. In TypeScript 4.7+, you can use an instantiation expression to capture the desired generics. In earlier TS versions, however, you may need to call the selector directly to get correct type inference:
// this AtomSelector takes a generic (T):
const addExtraData = <T extends any[]>({ get }: AtomGetters, list: T) => {
return list.map(item => ({
initialData: item,
extraData: get(extraDataAtom),
}))
}
const list = [{ name: 'a' }, { name: 'b' }]
// Using instantiation expressions (TS versions >= 4.7):
const withExtraData = useAtomSelector(addExtraData<typeof list>, list)
// With TS versions < 4.7, you'll need to call the selector directly:
const withExtraData2 = useAtomSelector(getters => addExtraData(getters, list))
Note that these are not exactly equivalent. Calling the selector directly essentially merges the called selector into the current selector or atom instance - the selector won't get its own node in the graph and will instead update the dependency graph of the current selector or atom instance. The above example works around that by wrapping the call in an inline selector, but that is undesirable in its own right.
While undesirable, it isn't necessarily a problem. Still, prefer using instantiation expressions if they're available for you.