Post cover

A Guide to Jotai: the Minimalist React State Management Library

For a long time, Redux had been the leader library of global state management in React. But with the introduction of hooks, I have found that libraries like react-query or useSWR() handle the fetching of data with less boilerplate.

But the simple UI state like side-menu expand, theme, dark-mode, etc. require separate management — in which case a simple global state management library like Jotai (https://github.com/pmndrs/jotai) becomes handy.

In this post, you will learn how to use Jotai.

Before I go on, let me recommend something to you.

The path to becoming proficient in React isn't easy... but fortunately with a good teacher you can shortcut.

Take the course "React Front To Back Course" by Brad Traversy to improve your React skills in a fun and practical way. Use the coupon code DMITRI and get your 20% discount!

1. Search query: a global state variable

An application has a header and main content components. The header component has an input field where the user can introduce a search query. The main component should display the search query introduced in the input field.

Here's the initial sketch of the application:


import { useState } from 'react';
function App() {
return (
<div>
<Header />
<Main />
</div>
);
}
function Header() {
const [search, setSearch] = useState('');
const handleChange = event => setSearch(event.target.value);
return (
<header>
<input type="text" value={search} onChange={handleChange} />
</header>
);
}
function Main() {
// How to access the search?
return <main>Search query: ???</main>;
}

Open the demo.

<App> is composed of 2 components: <Header> and <Main>.

<Header> is a component that contains an input field where the user introduces a search query.

<Main> is the component that should render the query entered into the input field. How would you access the value here?

The search query is a global state variable. And Jotai library can help you here using a construction named atom.

2. Jotai atoms

A piece of state in Jotai is represented by an atom. An atom accepts an initial value, be it a primitive type like a number, string, or more complex structures like arrays and objects.


import { atom } from 'jotai';
const counterAtom = atom(0);

counterAtom is the atom that holds the counter state.

But the atom alone doesn't help much. To read and update the atom's state Jotai provides a special hook useAtom():


import { atom, useAtom } from 'jotai';
export const counterAtom = atom(0);
export function CounterButton() {
const [count, setCount] = useAtom(counterAtom);
const handleClick = () => {
setCount(number => number + 1); // Increment number
};
return (
<div>
{count}
<button onClick={handleClick}>Increment</button>
</div>
);
}

Open the demo.

const [count, setCount] = useAtom(counterAtom) returns a tuple where the first item is the value of the state, and the second is a state updater function.

count contains the atom's value, while setCount() can be used to update the atom's value.

The selling point of atoms is that you can access the same atom from multiple components. If a component updates the atom, then all the components that read this atom are updated. This is the global state management!

For example, let's read counterAtom value in an another component <CurrentCount>:


import { useAtom } from 'jotai';
import { counterAtom } from './Button';
function CurrentCount() {
const [count] = useAtom(counterAtom);
return <div>Current count: {count}</div>;
}

Open the demo.

When the value of counterAtom changes (due to counter increment), then both components <CounterButton> and <CurrentCount> are going to re-render.

Jotai atom

What's great about useAtom(atom) hook keeps the same API as the built-in useState() hook — which also returns a tuple of state value and an updater function.

2.1 Search query atom

Now let's return to the problem of section 1: how to share the search query from the <Header> component in <Main> component.

You might already see the solution: let's create an atom searchAtom and share it between <Header> and <Main> components:


import { useState } from 'react';
import { atom, useAtom } from 'jotai';
function App() {
return (
<div>
<Header />
<Main />
</div>
);
}
const searchAtom = atom('');
function Header() {
const [search, setSearch] = useAtom(searchAtom);
const handleChange = event => setSearch(event.target.value);
return (
<header>
<input type="text" value={search} onChange={handleChange} />
</header>
);
}
function Main() {
const [search] = useAtom(searchAtom);
return <main>Search query: "{search}"</main>;
}

Open the demo.

const searchAtom = atom('') creates the atom that's going to hold the search query global state variable.

Inside of the <Header> component const [search, setSearch] = useAtom(searchAtom) returns the current search value, as well as the updater function.

As soon as the user types into the input field, handleChange() event handler updates the atom value: setSearch(event.target.value).

<Main> component can also access the searchAtom value: const [search] = useAtom(searchAtom). And when the atom's value changes due to the user typing into the input, <Main> component is updated to receive the new value.

In conclusion, atoms are global state pieces that can be accessed and modified by any component.

3. Jotai derived atoms

If you find yourself calculating data from an atom's value, then you may find useful the derived atoms feature of Jotai.

You can create a derived atom when supplying a callback function to atom(get => get(myAtom)): in which case Jotai invokes the callback with a getter function get from where you can extract the value of the base atom get(myAtom).


import { atom } from 'jotai';
const numberAtom = atom(2);
const isEvenAtom = atom(get => get(numberAtom) % 2 === 0);

In the example above numberAtom holds a number. isEvenAtom is a derived atom that determines whether the number stored in numberAtom is even.

Derived Atom

Of course, as soon as the base atom changes, the derived atom changes too.

For example, let's create isNameEmptyAtom derived atom that determines the string stored in nameAtom is empty:


import { atom, useAtom } from 'jotai';
const nameAtom = atom('Batman');
const isNameEmptyAtom = atom(get => get(nameAtom).length === 0);
function HeroName() {
const [name, setName] = useAtom(nameAtom);
const [isNameEmpty] = useAtom(isNameEmptyAtom);
const handleChange = event => setName(event.target.value);
return (
<div>
<input type="text" value={name} onChange={handleChange} />
<div>Is name empty: {isNameEmpty ? 'Yes' : 'No'}</div>
</div>
);
}

Open the demo.

What's even better is that you can create a derived atom from multiple base atoms!


import { atom } from 'jotai';
const counterAtom1 = atom(0);
const counterAtom2 = atom(0);
const sumAtom = atom((get) => get(counterAtom1) + get(counterAtom2));

sumAtom is derived from 2 base atoms: counterAtom1 and counterAtom2.

Derived From Multiple Base Atoms

4. Conclusion

I like Jotai for its minimalistic but flexible way to manage a simple global state.

To create a global state variable you need 2 steps:

A) Define the atom for your global state variable:


const myAtom = atom(<initialValue>);

B) Then access the atom's value and updater function inside of the component using the special hook useAtom(<atom>):


function MyComponent() {
const [value, setValue] = useAtom(myAtom);
// ...
}

I found that Jotai fits well to manage simple global variables, as a complement to asynchronous state management libraries like react-query and useSWR().

The post has described the basic functionality of Jotai. Visit the repository https://github.com/pmndrs/jotai to read about all the features.

Would you use Jotai to manage simple global state variables? What features, in your opinion, this library is still missing?

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom 😉. Living in the sunny Barcelona. 🇪🇸