Getting Started
This article assumes the following knowledge:
- Basic understanding of jotai
- You have seen the Large Object concept on the official jotai documentation
Goal
- Understand how to focus on deeply nested parts of a large object
- Not repeat what jotai library already explains
TLDR;
full code example:
https://codesandbox.io/s/pensive-fast-89dgy?file=/src/App.js
Introduction
Managing large objects is sometimes necessary when dealing with structured client state. For eg. managing a very sophisticated tree or maybe a content editor. Jotai makes this very simple with its various utilities and plugins to manage state.
Utilities and tools that will be discussed in this article:
- focusAtom (jotai/optic-ts integration) - create a read-write derived atom based on a predicate
- splitAtom (jotai utility) - transform a data array into an array of atoms
- selectAtom (jotai utility) - create a read-only atom based on a predicate
Challenges with managing large objects with only react
- Managing updates to specific parts that are deeply nested and reporting the change back to the main object
- Managing deeply nested callbacks and also changing the data manually with setters, spreading, you name it!
None of the challenges listed above are inherently bad but when a developer does repetitive work, the thought always comes to mind about how all this boilerplate can be abstracted away.
Luckily jotai has a nice solution to these challenges.
Let’s talk about doing this the jotai way.
For this article we will be managing a cat! Yes...meow! The application will tell a Vet if specific body parts of the cat are injured.
Please note the object shape below is not necessarily how you would do this in a real application but designed to give a good example for the purposes of this article
Now what will our cat injury data look like?
{
"name": "Sparkles",
"owner": { "id": 1, "firstName": "John", "lastName": "Doe" },
"parts": [
{
"type": "legs",
"attributes": [
{ "placement": "front-left", "injured": false },
{ "placement": "front-right", "injured": false },
{ "placement": "back-left", "injured": false },
{ "placement": "back-right", "injured": true }
]
},
{
"type": "tail",
"attributes": [{ "injured": true }]
},
{
"type": "ears",
"attributes": [
{ "placement": "left", "injured": false },
{ "placement": "right", "injured": true }
]
}
]
}
First let's understand two types of state within large objects and how to access them
- View only state
For state that we only need to view, we can use the selectAtom to access them. The selectAtom will give you a read-only atom that works well for this scenario.
- Editable state
For state that we need to edit, we can use the focusAtom to literally focus on these sections and edit them without large callbacks to merge the data back into the main object.
Jotai documentation already explains how to go at least one level deep, so the question you may ask is, how do we get to the nested arrays like the cat’s attributes and manage the data individually?
You may be tempted to split the attributes array using splitAtom, however, splitAtom only creates atoms from raw data and this data has no way of knowing how to report itself back to the root node.
So how do we update each “cat attribute” without managing the entire array ourselves?
The trick lies within the optic-ts integration.
You can focus on array indexes using the at(index) function which keeps an established reference to the root node.
See code example below.
const useAttributeAtom = ({ attributesAtom, index }) => {
return useMemo(() => {
return focusAtom(attributesAtom, (optic) => optic.at(index));
}, [attributesAtom, index]);
};
const Attribute = ({ attributesAtom, index }) => {
const attributeAtom = useAttributeAtom({ attributesAtom, index });
const [attribute, setAttribute] = useAtom(attributeAtom);
return (
<div style={{ display: "flex" }}>
<label>
<span style={{ marginRight: "16px" }}>
{attribute.placement}
</span>
<Switch
onChange={(checked) =>
setAttribute((prevAttribute) => ({
...prevAttribute,
injured: checked
}))
}
checked={attribute.injured}
/>
</label>
</div>
);
};
See the full code example
What did we achieve?
- We were able to change focused pieces of the large object without deep prop drilling of any onChange functions
- We managed “global” state within the application while keeping the interfaces resembling React.
Important Tips!
- The starting atom (root node) must be a writable atom. This helps when derived atoms need to write back changed information
- Atoms created within a render should be memoized or else you will have too many re-renders and most likely React will throw an error stating this exactly.
Thanks for reading!
Have you had this problem before?
Let me know if you have done this with jotai before and what solutions you came up with.
Always looking to learn more!
Top comments (2)
Nice article! I was hoping to optimize re-renders with focused atoms but it seams it does not do that, from what I could test with your code. So it was useful but I'm still looking for a solution.
Edit:
I just realized that the top level component was using "useAtom" with the whole atom for monitoring purposes. All components were then re-rendering each time one was updated. This is expected behaviour considering the subscription to the atom. When removing this I can see that focused atoms will not trigger other focused atoms when updated.
I'd like to see this in typescript.