DEV Community

Cover image for Advanced Large Object Management with jotai
Layton Whiteley
Layton Whiteley

Posted on • Edited on

Advanced Large Object Management with jotai

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 }
     ]
   }
 ]
}
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode
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>
 );
};
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
zjeff222 profile image
JF • Edited

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.

Collapse
 
arivera618 profile image
arivera618

I'd like to see this in typescript.