JotaiJotai

状態
Primitive and flexible state management for React

Performance

Optimizing re-render

Atoms are the unit of triggering re-renders, so atoms should be small enough to reduce extra re-renders

// bad practice
const objAtom = atom({ x: 0, y: 0 })
const Component1 = () => {
const [value, setValue] = useAtom(objAtom) // this will trigger re-renders either `x` or `y` changes
...
}
// good practice
const 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 practice
const objAtom = atom({ x: 0, y: 0 })
const derived1Atom = atom((get) => get(objAtom).x * 2) // extra evaluation when only `y` changes
// good practice
const 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:

  1. start multiple async read at once in a higher component in the tree. usePrepareAtoms is available in jotai-suspense.
  2. 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 overuse React.memo as, for many cases, atoms are enough to optimize re-renders.