Skip to main content

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:

You can also call them directly, passing an AtomGetters object. There is one situation where this can be necessary.

tip

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,
})
tip

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:

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.

See Also