Skip to content

Jotai, a beautiful state manager for React devs

On this page

Introduction

Jotai is an awesome state manager for React. There are no extra re-renders, state resides within React, and you get the full benefits of Suspense and concurrent features. It scales from a simple React.useState replacement to a large-scale application with complex requirements.

Jotai takes a bottom-up approach to React state management with an atomic model inspired by Recoil. One can build state by combining atoms and renders are optimized based on atom dependency. This solves the extra re-render issue of React context and eliminates the need for the memoization technique.

A long time ago, I started learning frontend web development. There was so much to learn, and one of the most important topics was state management.

Using Jotai

In React, I can declare a state by using useState.

TSX

const [count, setCount] = useState(0);

The problem is that as my app gets bigger, I need to pass the state down through component props. Otherwise, React context might help.

But React context isn’t an awesome solution for me. I need to wrap my components inside its provider, which you might already know as Provider Hell. React context also re-renders my components every time the state changes.

Jotai, on the other hand, works like useState. I don’t need to wrap my app with a Provider, and it uses an atomic approach, which I’ve been using a lot lately (components, styles, services, etc.).

Declaring State in a Store File

In Jotai, I can declare my app’s state with an atom.

@/store/atom.ts

import { atom } from "jotai";

type User = {
  name: string;
  gender: "male" | "female";
  website: string;
};

export const userAtom = atom<User | null>(null);

Then I can use the state in my component.

TSX

import { userAtom } from "@/store/atom";

import { useAtom } from "jotai";

const [user, setUser] = useAtom(userAtom);

The user variable contains the user’s data, or null if no user is logged in. So what does setUser do? Well, setUser is just like setState in React: it’s a function that modifies the state.

Just like React’s setState, setUser accepts either a function or a new state. I can replace the state with a new one, like this.

TSX

import { userAtom } from "@/store/atom";

import { useAtom } from "jotai";

const [user, setUser] = useAtom(userAtom);

// replace with newer state
const updateUser = () =>
  setUser({ name: "Foo", gender: "male", website: "foo.com" });

Or maybe I only want to modify certain properties. In this case, I previously declared a type User, which is an object with the properties name, gender, and website.

TSX

import { userAtom } from "@/store/atom";

import { useAtom } from "jotai";

const [user, setUser] = useAtom(userAtom);

// modify only name property
const updateUser = () =>
  setUser((prevState) => ({ ...prevState, name: "FooBizz" }));

In the updateUser function, I change the name property to FooBizz. That’s the basic usage of Jotai.

See? It’s just like a regular useState, and I can also use the User state in another component. Cool, right?😎

Persistent State

Sometimes when I refresh my browser, my app’s state gets reset too. But I want to keep my state, and the solution here is persistent state.

Jotai comes with batteries-included utilities. For example, atomWithStorage not only holds my state but also saves it to localStorage. This is called persistent state: whenever I modify my state, the value in localStorage updates too.

This is very useful for state that needs to live in localStorage, like Theme.

@/store/atom.ts

import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

type User = {
  name: string;
  gender: "male" | "female";
  website: string;
};
type Theme = "dark" | "light";

export const userAtom = atom<User | null>(null);

// atomWithStorage(localStorage key, initial value)
export const themeAtom = atomWithStorage<Theme>("theme", "light");

I’m exporting a new atom with the atomWithStorage() utility. This function accepts 3 arguments. The first two are required and the last one is optional. The first argument is the localStorage key, and the second is the initial value of the state.

But how do I use this state? Well, again, I can read and modify it just like a regular useState.

TSX

import { themeAtom } from "@/store/atom";

import { useAtom } from "jotai";

const [theme, setTheme] = useAtom(themeAtom);

Comparison to Other Libraries

Let’s compare Jotai to other libraries. This comparison is based on the official Jotai website.

Jotai vs Zustand

Jotai is like Recoil, while Zustand, opens in a new tab is like Redux.

The major difference is the state model. Zustand is a single store (although you could create multiple separate stores), while Jotai consists of primitive atoms and allows composing them together. In this sense, it’s a matter of programming mental models.

Jotai can be a replacement for useState+useContext. Instead of creating multiple contexts, atoms share one big context.

Zustand is an external store and the hook is to connect the external world to the React world.

When to Switch

  • If you need a replacement for useState+useContext, Jotai fits well.
  • If you want to update state outside React, Zustand works better.
  • If code splitting is important, Jotai should perform well.
  • If you prefer Redux devtools, Zustand is good to go.
  • If you want to make use of Suspense, Jotai is the one.

How is Jotai Different from Recoil?

Developer

  • Jotai is developed collectively by a few developers in the Poimandres (formerly react-spring) org.
  • Recoil is developed by a team at Facebook.

Basis

  • Jotai focuses on primitive APIs for easy learning, and it’s unopinionated (the same philosophy as Zustand).
  • Recoil is full-featured for big apps with complex requirements.

Technical Difference

  • Jotai depends on atom object referential identities.
  • Recoil depends on atom string keys.

When to Use Which

  • If you want to learn something new, either should work.
  • If you like Zustand, Jotai would also be pleasant.
  • If your app heavily requires state serialization (storing state in storage, server, or URL), Recoil comes with good features.
  • If you need React Context alternatives, Jotai comes with enough features.
  • If you want to create a new library, Jotai might give you good primitives.
  • Otherwise, both are pretty similar in their general goals and basic techniques.

Using it with React

I can store my app’s state in each component, but I can also create a store in a separate file. I prefer to create a new file inside a store folder, for example:

@/store/atom.ts

import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

type Theme = "light" | "dark";
type User = {
  name: string;
  gender: "male" | "female";
  website: string;
};

export const userAtom = atom<User | null>(null);

export const themeAtom = atomWithStorage<Theme>("theme", "light");

Read and Modify State

I can read and modify the state in any component like this.

@/components/DarkModeToggle.tsx

import * as atoms from "@/store/atom";

import { useAtom } from "jotai";

export default function DarkModeToggle() {
  const [theme, setTheme] = useAtom(atoms.themeAtom);

  const toggleTheme = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

  return <button onClick={toggleTheme}>Current theme is {theme}</button>;
}

But I prefer to create a custom hook instead. For example, to manage my theme state, I can create a custom hook that switches between dark mode and light mode.

@/hooks/useDarkMode.ts

import * as atoms from "@/store/atom";

import { useAtom } from "jotai";
import { useEffect } from "react";

const useDarkMode = () => {
  const [theme, setTheme] = useAtom(atoms.themeAtom);

  const toggleTheme = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

  useEffect(() => {
    document.documentElement.className = theme;
    document.documentElement.style.colorScheme = theme;
  }, [theme]);

  return { theme, toggleTheme };
};

export default useDarkMode;

Then I can refactor the DarkModeToggle component above to look like this:

@/components/DarkModeToggle.tsx

import useDarkMode from "@/hooks/useDarkMode";

export default function DarkModeToggle() {
  const { theme, toggleTheme } = useDarkMode();

  return <button onClick={toggleTheme}>Current theme is {theme}</button>;
}

See? Very simple, right? Oh, and you can read the complete Jotai documentation here, opens in a new tab.

The core concepts and API reference are very easy to learn. Also, check out this free course, opens in a new tab that Jotai provides.

So that’s it, folks. Thank you so much for reading, and I hope this post gave you more insight into Jotai. See you next time👋

Enjoyed this note?