Paul is a Senior Software Engineer, Independent Developer Advocate and Technical Writer. More from Paul can be found on his site, paulie.dev.
Read more from Paul Scanlon
If you’ve been using React for a while, you’ll no doubt have experienced scenarios where global state is required. In this article, I’ll explain how you can use Jotai, from Daishi Kato (creator of Waku), to implement really simple, but intuitive, global state for your React applications.
Global state differs from local state, which is confined to a specific component. Global state can be shared across multiple components providing read and write functionality.
There are a number of existing solutions that help provide global state in React — you may already be familiar with React’s Context API, Redux, MobX, or React’s useState and useReducer hooks.
Jotai is a primitive and flexible state management solution for React.
Jotai gives you a minimalistic API that you can use to separate your state management from your UI. You get all the benefits of a well-managed state, and you don’t have to set up any boilerplate, like defining actions, reducers, dispatchers or stores.
Jotai gives you a minimalistic API that you can use to separate your state management from your UI.
Jotai was born to solve extra re-render issues in React. An extra re-render is when the render process produces the same UI result, where users won’t see any differences. You can view Jotai as a drop-in replacement to React’s useContext
, except with Jotai you create an atom and you’re off to the races.
Here I’ll cover the basics of using Jotai with Waku, which is predominantly a server-side React framework, but by using the 'use client'
React directive, it can also support client-side interactivity.
Jotai, in its simplest form, can be used where an atom and its default value are defined and exported from a shared “state” file.
From within a component you can now import the atom and pass it onto the useAtom hook. The atom’s value is then readable from the count
constant.
In this example, the div containing the count
would display the atom’s initial value of 0. You could import and read this value from any component within your application, just as you would with an “ordinary” const
or enum
, etc. The difference is an atom is also writable.
To write to an atom, the declaration can be extended to include both the readable value and an update function as a tuple, just like useState
in React.
In this example, an update function has been included in the const declaration and an event handler has been added to handle the button click. The setCount
update function has access to the current count
value, which can then be incremented by 1 each time the button is pressed.
The updated count
value can be displayed by this component and also the read-only component.
Dealing with primitives is pretty straightforward, but what about objects?
As before, a new atom can be defined within the global “state” file.
To update an object atom, the approach is similar to React, where you can use a spread operator (...)
to create a shallow copy of the object, and then update a value contained within the object. In this case, the test
value will be set to the value provided by the checkbox (event.target.checked
), which will either equal true
or false
. Any other key-value pairs from the object, e.g., foo: 'bar'
, would remain as they are.
As before with the primitive atom example, this object could be read from any other component in the application.
It’s quite likely that in modern applications an initial value would be set as the result of a server-side request. In these scenarios, useHydrateAtoms can be used to update the client-side state with values received as a result of a server-side request.
In this scenario, you would define the atom in the global state file and set the default values, e.g.:
Then, from a route-level server-side request, you can pass the data onto a component, e.g.:
From within a component useHydrateAtoms can be used to update the default global state values with values from the server-side request, which in this case have been passed to the component via a prop named userData
.
To access the updated values, the useAtom
hook can be used in the same way as before.
However, to use the useHydrateAtoms
hook with server-side data, Jotai’s Provider component must also be implemented at the root level of your application. Kato gave the following explanation: “With Provider, a store is created within a tree. Without it, a “global” store is used. Client-side works with one user per memory. But server-side, multiple users share the global store. It’s a huge potential issue.”
The Provider component implementation would look something like this:
Another place where you may want to read and write data from is localStorage. Jotai makes this easy too, with the atomWithStorage util. Here’s an example that toggles “dark mode.”
As before, a new atom is defined in the “state” file, but this time the atom is declared using atomWithStorage
, which can be imported from jotai/utils
.
In this example, the useAtom
hook is used and provided the value for darkModeAtom
. This makes the value available to the application and adds the value to the user’s browser/localStorage.
When declaring a new atomWithStorage
, two parameters are required. The first is the name of the storage item; the second is the value — which can be seen in the browser’s developer tools.
These are just a few ways you can use Jotai. There are a number of other recipes in the docs that detail other ways you can use it. Hopefully you’ll agree: Jotai really simplifies global state management in React.
If you’re interested, there’s also a good comparison in the documentation that shows the React Context API approach compared to the Jotai approach.