Atom creators
atomWithToggle
atomWithToggle
creates a new atom with a boolean as initial state & a setter function to toggle it.
This avoids the boilerplate of having to setup another atom just to update the state of the first.
import { WritableAtom, atom } from 'jotai'export function atomWithToggle(initialValue?: boolean): WritableAtom<boolean, boolean | undefined> {const anAtom = atom(initialValue, (get, set, nextValue?: boolean) => {const update = nextValue ?? !get(anAtom)set(anAtom, update)})return anAtom as WritableAtom<boolean, boolean | undefined>}
An optional initial state can be provided as first argument.
The setter function can have an optional arg to force a particular state (if you want to make a setActive function out of it for example).
Here is how it's used.
import { atomWithToggle } from 'XXX'// will have an initial value set to trueconst isActiveAtom = atomWithToggle(true)
And in a component :
const Toggle = () => {const [isActive, toggle] = useAtom(isActiveAtom)return (<><button onClick={() => toggle()}>isActive: {isActive ? 'yes' : 'no'}</button><button onClick={() => toggle(true)}>force true</button><button onClick={() => toggle(false)}>force false</button></>)}
atomWithToggleAndStorage
atomWithToggleAndStorage
does the same asatomWithToggle
but also persist the state anytime it changes in given storage usingatomWithStorage
.
Here is the source :
import { WritableAtom, atom } from 'jotai'import { atomWithStorage } from 'jotai/utils'export function atomWithToggleAndStorage(key: string,initialValue?: boolean,storage?: any): WritableAtom<boolean, boolean | undefined> {const anAtom = atomWithStorage(key, initialValue, storage)const derivedAtom = atom((get) => get(anAtom),(get, set, nextValue?: boolean) => {const update = nextValue ?? !get(anAtom)set(anAtom, update)})return derivedAtom}
And how it's used :
import { atomWithToggleAndStorage } from 'XXX'// will have an initial value set to false & be stored in localStorage under the key "isActive"const isActiveAtom = atomWithToggleAndStorage('isActive')
The usage in a component is also the same as atomWithToggle
.
atomWithCompare
atomWithCompare
creates atom that only triggers updates when custom compare functionareEqual(prev, next)
is false.
This can help you avoid unwanted re-renders by ignoring state changes that don't matter to your application.
Note: Jotai uses Object.is
internally to compare values when changes occur. If areEqual(a, b)
returns false, but Object.is(a, b)
returns true, Jotai will not trigger an update.
import { atomWithReducer } from 'jotai/utils'export function atomWithCompare<Value>(initialValue: Value,areEqual: (prev: Value, next: Value) => boolean) {return atomWithReducer(initialValue, (prev: Value, next: Value) => {if (areEqual(prev, next)) {return prev}return next})}
Here's how you'd use it to implement an atom that ignores updates that are shallow-equal:
import { atomWithCompare } from 'XXX'import { shallowEquals } from 'YYY'import { CSSProperties } from 'react'const styleAtom = atomWithCompare<CSSProperties>({ backgroundColor: 'blue' },shallowEquals)
In a component:
const StylePreview = () => {const [styles, setStyles] = useAtom(styleAtom)return (<div><div styles={styles}>Style preview</div>{/* Clicking this button twice will only trigger one render */}<button onClick={() => setStyles({ ...styles, backgroundColor: 'red' })}>Set background to red</button>{/* Clicking this button twice will only trigger one render */}<button onClick={() => setStyles({ ...styles, fontSize: 32 })}>Enlarge font</button></div>)}
atomWithRefresh
atomWithRefresh
creates a derived atom that can be force-refreshed, by using the update function.
This is helpful when you need to refresh asynchronous data after performing a side effect.
It can also be used to implement "pull to refresh" functionality.
import { atom, Getter } from 'jotai'export function atomWithRefresh<T>(fn: (get: Getter) => T) {const refreshCounter = atom(0)return atom((get) => {get(refreshCounter)return fn(get)},(_, set) => set(refreshCounter, (i) => i + 1))}
Here's how you'd use it to implement an refreshable source of data:
import { atomWithRefresh } from 'XXX'const postsAtom = atomWithRefresh((get) =>fetch('https://jsonplaceholder.typicode.com/posts').then((r) => r.json()))
In a component:
const PostsList = () => {const [posts, refreshPosts] = useAtom(postsAtom)return (<div><ul>{posts.map((post: any) => (<li key={post.id}>{post.title}</li>))}</ul>{/* Clicking this button will re-fetch the posts */}<button type="button" onClick={refreshPosts}>Refresh posts</button></div>)}
atomWithListeners
atomWithListeners
creates an atom and a hook. The hook can be called to add a new listener. The hook takes as an argument a callback, and that callback will be called every time the atom's value is set. The hook also returns a function to remove the listener.
This can be useful when you want to create a component that can listen to when an atom's state changes without having to re-render that component with each of those state changes.
import { useEffect } from 'react'import { atom, Getter, Setter, SetStateAction } from 'jotai'import { useUpdateAtom } from 'jotai/utils'type Callback<Value> = (get: Getter,set: Setter,newVal: Value,prevVal: Value,) => voidexport function atomWithListeners<Value>(initialValue: Value) {const baseAtom = atom(initialValue)const listenersAtom = atom(<Callback<Value>[]>[])const anAtom = atom((get) => get(baseAtom),(get, set, arg: SetStateAction<Value>) => {const prevVal = get(baseAtom)set(baseAtom, arg)const newVal = get(baseAtom)get(listenersAtom).forEach((callback) => {callback(get, set, newVal, prevVal)})},)const useListener = (callback: Callback<Value>) => {const setListeners = useUpdateAtom(listenersAtom)useEffect(() => {setListeners((prev) => [...prev, callback])return () =>setListeners((prev) => {const index = prev.indexOf(callback)return [...prev.slice(0, index), ...prev.slice(index + 1)]})}, [setListeners, callback])}return [anAtom, useListener] as const}
In a component:
const [countAtom, useCountListener] = atomWithListeners(0)function EvenCounter() {const [evenCount, setEvenCount] = useState(0)useCountListener(useCallback((get, set, newVal, prevVal) => {// Every time `countAtom`'s value is set, we check if its new value// is even, and if it is, we increment `evenCount`.if (newVal % 2 === 0) {setEvenCount((c) => c + 1)}},[setEvenCount],),)return <>Count was set to an even number {evenCount} times.</>}