Performance
Jotai & React gives us quite a few tools to manage the re-renders that happen in the app lifecycle. First, please read about the difference between render & commit, because that's very important to understand before going further.
Cheap renders
As seen in the core section, React 18 default behaviour (and overall good practice) is to make sure your component functions are idempotent. They will be called multiple times in the render phase, even at mount. So we need to keep our renders cheap at all cost!
Heavy computation
Always make heavy computation outside of the React lifecycle (in actions for example)
Dont's:
// Heavy computation for each itemconst selector = (s) => s.filter(heavyComputation)const Profile = () => {const [computed] = useAtom(selectAtom(friendsAtom, selector))}
Do's:
const friendsAtom = atom([])const fetchFriendsAtom = atom(null, async (get, set, payload) => {// Fetch all friendsconst res = await fetch('https://...')// Make heavy computation once onlyconst computed = res.filter(heavyComputation)set(friendsAtom, computed)})// Usage in componentsconst Profile = () => {const [friends] = useAtom(friendsAtom)}
Small components
Observed atoms should only re-render small parts of your application that required an update. The less comparison React has to make, the shorter your render time will be.
Dont's:
const Profile = () => {const [name] = useAtom(nameAtom)const [ageAtom] = useAtom(ageAtom)return (<><div>{name}</div><div>{age}</div></>)}
Do's:
const NameComponent = () => {const [name] = useAtom(nameAtom)return (<div>{name}</div>)}const AgeComponent = () => {const [age] = useAtom(ageAtom)return (<div>{age}</div>)}const Profile = () => {return (<><NameComponent /><AgeComponent /></>)}
Render on demand
Usually, the main performance overhead will come from re-rendering parts of your app that did not need to, or way more than they should.
We have
First is to always , because React works by calling your component multiple times to check for new commits to do. Always remember to make your renders as cheap as possible.
Second is to use the Jotai & React tools to prevent (heavy) re-renders.
- You can break down large object atoms to more primitive atoms
selectAtom
subscribe to specific part of a large object and only re-render on value changefocusAtom
same as selectAtom, but creating a new atom for the part, giving a setter to update that specific part easilyuseMemo
&useCallback
you can always use these to limit changes to a specific array of dependencies
Frequent or rare updates
Ask yourself whether your atom is usually going to be frequently update or more rarely.
Let's imagine you store in an atom an object that changes almost every second, it may not be best suited to "focus" on a specific properties of this object using focusAtom
, because anyway they will all re-render in the same time, so best adding no overhead and not create any more atoms.
On the other hand, if your object has properties that rarely change, and most importantly, that change independently from the other properties, then you may want to use focusAtom
or selectAtom
to prevent un-necessary renders.
"Stop observing" pattern
An example of pattern that can be interesting is to use useMemo
to read an atom value only once, in order to prevent further renders even if the atom changes down the line.
Let's imagine a case, you have a list of toggles. Let's view 2 approaches for it:
Standard pattern
We create our store of 3 toggles set to false
const togglesAtom = atom([false, false, false])
Then, when the user clicks one toggle, we update it
const Item = ({ index, val }) => {const setToggles = useSetAtom(togglesAtom)const onPress = () => {setToggles(old => [...old, [index]: !val])}}const List = () => {const [toggles] = useAtom(togglesAtom)return toggles.map((val, index) => <Item id={index} val={val} />)}
With this approach, updating any toggle will cause all <Item />
to re-render.
Memoized pattern
Now let's try to memoize the value on the first render
const List = () => {const [toggles] = useMemo(() => useAtom(togglesAtom), [])return toggles.map((val, index) => <Item id={index} initialValue={val} />)}
But now it means we have to handle the changes locally in each Item
const Item = ({ index, initialValue }) => {const setToggles = useSetAtom(togglesAtom)const [toggle, setToggle] = React.useMemo() => useAtom(atom(val)), [])const onPress = () => {setToggle(!toggle) // Update locallysetToggles(old => [...old, [index]: !val]) // Update the main atom}}
Now if you update one toggle, it will only re-render the corresponding <Item />