The atomWithListeners pattern allows you to create an atom and a corresponding hook that can listen to state changes. This is particularly useful for components that need to perform side effects (like updating other state or triggering external APIs) when an atom changes, but do not need to re-render themselves every time that atom's value updates.
atomWithListeners returns a tuple containing:
- An atom: A standard Jotai atom that holds the value.
- A hook (
useListener): A hook that accepts a callback. This callback is executed every time the atom's value is set. The callback receives the get function, set function, the newVal, and the prevVal.
The hook also returns a cleanup function (via useEffect) to automatically remove the listener when the component unmounts.
import { useEffect } from 'react'
import {
atom,
useAtom,
useSetAtom,
Getter,
Setter,
SetStateAction,
} from 'jotai'
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 = useSetAtom(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
}