Performance
Optimizing re-render
Atoms are the unit of triggering re-renders, so atoms should be small enough to reduce extra re-renders
// bad practiceconst objAtom = atom({ x: 0, y: 0 })const Component1 = () => {const [value, setValue] = useAtom(objAtom) // this will trigger re-renders either `x` or `y` changes...}// good practiceconst xAtom = atom(0)const yAtom = atom(0)const Component2 = () => {const [x, setX] = useAtom(xAtom)const [y, setY] = useAtom(yAtom)...}
That's general guideline, but there are exceptions. If you always use xAtom
and yAtom
together, the combined objAtom
is better because useAtom
creates a subscription which is an overhead (which is far smaller than an extra re-render).
Likewise, derived atoms (custom read
) are re-evaluated when dependency atoms change
// bad practiceconst objAtom = atom({ x: 0, y: 0 })const derived1Atom = atom((get) => get(objAtom).x * 2) // extra evaluation when only `y` changes// good practiceconst xAtom = atom(0)const yAtom = atom(0)const derived2Atom = atom((get) => get(xAtom) * 2)
Using big atoms
Sometimes, we want to create an atom with large object in it. For example, if you use atomWithStorage
or an atom to be sync with server data.
In this case, you can create derived atoms to reduce extra re-renders.
const objAtom = atom({ x: 0, y: 0 })const xAtom = atom((get) => get(objAtom).x)const Component = () => {const [x, setX] = useAtom(xAtom)...}
You can also make the xAtom
writable with write
function.
Some additional functions such as focusAtom
and splitAtom
help this use case.
Creating atoms dynamically
Creating atoms with the atom
function is very lightweight.
So, feel free to create an atom and throw it away.
When we create an atom in React render function (components/hooks), we should use useMemo
or something. Otherwise, useAtom may cause infinite loop.
const objAtom = atom({ x: 0, y: 0 })const xAtom = atom((get) => get(objAtom).x)const Component = ({ isX }) => {const derivedAtom = useMemo(() => atom((get) => get(objAtom)[isX ? 'x' : 'y']),[isX])const [value, setValue] = useAtom(derivedAtom)...}
Notes about concurrent rendering
read
function of derived atoms are evaluated in the render phase.- if
read
function takes time, concurrent rendering may throw away the result, without commits (without browser paints). write
function of derived atoms are evaluated in sync.
Notes about render without commits
- jotai uses
useReducer
internally, and it's often the case derived atoms trigger re-renders without commits (no browser paints). - If this can be an issue, you want to consider splitting dependency atoms, adding intermediate atoms, or memoize function.
(Need code snippets?)
Notes about Suspense
Async read
function in derived atoms is very easy to use Suspense.
But, because read
function is evaluated in the render phase, it can be too late to start the async function, especially for data fetching. This can lead waterfalls and the performance gets really bad.
We are still working on best practices, but for now two approaches are:
- start multiple async
read
at once in a higher component in the tree.usePrepareAtoms
is available in jotai-suspense. - start async functions in
write
, and set a promise as an atom value. This will ideal performance-wise, but you need to know all dependent atoms and update all at once. (It doesn't seem very practical in a complex app.)
General React techniques
- use
useMemo
to avoid re-evaluate heavy computation React.memo
is useful for item components for an array (even if we use "atoms-in-atom" pattern. However, don't overuseReact.memo
as, for many cases, atoms are enough to optimize re-renders.