title | nav |
---|---|
Atom creators |
5.02 |
atomWithToggle
creates a new atom with a boolean as initial state & a setter function to toggle it.
This avoids the boilerplate of having to set up 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 the first argument.
The setter function can have an optional argument to force a particular state, such as if you want to make a setActive function out of it.
Here is how it's used.
import { atomWithToggle } from 'XXX'
// will have an initial value set to true
const 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
is likeatomWithToggle
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 & get stored in localStorage under the key "isActive"
const isActiveAtom = atomWithToggleAndStorage('isActive')
The usage in a component is also the same as atomWithToggle
.
atomWithCompare
creates atom that 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 make 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
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 refresh-able 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
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 is 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
) => void
export 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.</>
}