Skip to content
State Management

Jotai — Component Library Guide

Primitive and flexible state management for React. Atomic model inspired by Recoil - each atom is a piece of state, no store required.

TypeScript MIT
1.9M weekly downloads
18.6k GitHub stars
~4.7kB bundle size
v2.11.3 latest version
stateatomsatomicrecoil-likesuspense

TL;DR: Jotai is atomic state management for React. Define small atom() units, use useAtom() in any component - no Provider needed. Components re-render only when their specific atoms change. Simpler than Redux, lighter than Recoil. Official docs: jotai.org.

What is Jotai?

Jotai implements the atomic state model - state is broken into small independent units called atoms. Components subscribe to exactly the atoms they need, so a change to one atom only re-renders components that use that atom. No selectors, no context providers, no boilerplate.

When to use it

Use Jotai when:

  • You have many small, independent pieces of global state (e.g., per-entity UI state)
  • You want fine-grained re-renders without manually writing selectors
  • You like Recoil's model but want something lighter and better maintained

Jotai vs Zustand: Zustand is store-first (one object per domain); Jotai is atom-first. Zustand is simpler to reason about for most teams. Jotai shines when you have many orthogonal pieces of state that rarely interact.

Key Features

  • Atoms - the smallest unit of state; each is a useState equivalent
  • Derived atoms - compute values from other atoms without memoization boilerplate
  • Async atoms - atoms can be async; Jotai integrates with React Suspense
  • Atom families - create parameterised atoms (e.g., per-item state)
  • jotai/utils - atomWithStorage, atomWithReset, selectAtom, splitAtom

Installation

npm install jotai

Basic Usage

import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'

// Define atoms (outside components  -  these are module-level constants)
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)  // derived

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  const doubled = useAtomValue(doubledAtom)

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <p>Doubled: {doubled}</p>
    </div>
  )
}

// Component that only writes  -  doesn't re-render on change
function ResetButton() {
  const setCount = useSetAtom(countAtom)
  return <button onClick={() => setCount(0)}>Reset</button>
}

Atom Families (Per-Item State)

import { atomFamily } from 'jotai/utils'

// Create a separate atom for each todo item by ID
const todoAtomFamily = atomFamily(
  (id: string) => atom({ id, completed: false, text: '' })
)

function TodoItem({ id }: { id: string }) {
  const [todo, setTodo] = useAtom(todoAtomFamily(id))
  return (
    <label>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => setTodo(t => ({ ...t, completed: !t.completed }))}
      />
      {todo.text}
    </label>
  )
}

Async Atoms with Suspense

import { atom } from 'jotai'
import { Suspense } from 'react'

const userAtom = atom(async () => {
  const res = await fetch('/api/me')
  return res.json() as Promise<User>
})

function Profile() {
  const user = useAtomValue(userAtom)  // Suspense required above
  return <h1>{user.name}</h1>
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Profile />
    </Suspense>
  )
}

Common Gotchas

  • Define atoms outside components - atoms defined inside a component create a new atom on every render, which defeats the purpose
  • No Provider needed - but you can use <Provider> to scope atom state to a subtree (useful for tests or isolated widgets)
  • useAtomValue vs useAtom - prefer useAtomValue when you only read; prefer useSetAtom when you only write - both skip unnecessary re-renders

Why atomic state cuts re-renders

The whole pitch for Jotai comes down to one thing: components subscribe to atoms, not to a big store. With a single store object, any change forces every subscriber to run a selector and bail out. Jotai flips that. If your themeAtom changes, only the components that actually read themeAtom re-render. Everything else stays untouched.

This matters most in dashboards and editors where dozens of small, unrelated values live on screen at once. Picture a settings panel with 30 toggles. With Jotai, flipping one toggle re-renders one row. You don't write a single selector to get that behavior - it falls out of the atom model for free.

Derived atoms keep the same property. A derived atom only recomputes when one of its dependencies changes, and it only pushes updates to components that read the derived value. In my experience this removes a whole class of "why is this re-rendering" debugging sessions that you get with context-based state.

Reading and writing patterns you'll actually use

Read-write atoms cover most app state, but Jotai also supports write-only atoms for actions. A write-only atom takes null as its read value and a write function as its second argument. You call it like a reducer dispatch, which keeps your update logic in one place instead of scattered across components.

import { atom, useSetAtom } from 'jotai'

const countAtom = atom(0)

// Write-only "action" atom
const incrementAtom = atom(null, (get, set, by: number) => {
  set(countAtom, get(countAtom) + by)
})

function Buttons() {
  const increment = useSetAtom(incrementAtom)
  return (
    <>
      <button onClick={() => increment(1)}>+1</button>
      <button onClick={() => increment(10)}>+10</button>
    </>
  )
}

This pattern scales nicely. You keep business rules inside the action atom, and components stay dumb. They fire the action and never touch the raw state directly.

Persistence and storage

The jotai/utils package ships atomWithStorage, which syncs an atom to localStorage, sessionStorage, or any storage you pass in. It reads the initial value on mount and writes back on every change. Theme preference, sidebar collapse state, and recently viewed items are all good candidates.

import { atomWithStorage } from 'jotai/utils'

const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')

One honest caveat: server-side rendering needs care. The stored value isn't available during the first server render, so you can hit hydration mismatches. The usual fix is to render a neutral default until the component mounts, then read the stored value. Don't skip this step or React will warn you in the console.

Testing atoms in isolation

Because atoms work without a global setup, tests stay simple. Wrap the component under test in a fresh <Provider> to get an isolated atom store per test. Each test starts clean, so state from one case can't bleed into the next. You can also hydrate a Provider with initial atom values, which is handy for rendering a component in a specific state without clicking through the UI first.

  • Zustand - store-first alternative; Zustand is better for domain objects, Jotai for fine-grained per-value subscriptions
  • TanStack Query - Jotai handles client state; TanStack Query handles server/async state
  • TypeScript Generics Guide - atom<T>() is generic; typed atoms prevent runtime errors in complex state graphs

Frequently Asked Questions

What is the difference between Jotai and Zustand?

Jotai is atom-first - state is composed from small independent atoms. Zustand is store-first - you define one object per domain. Jotai produces finer re-render granularity automatically; Zustand is simpler to reason about for most teams. Choose Jotai when you have many orthogonal pieces of state.

Do I need a Provider with Jotai?

No. Atoms work globally without a Provider. Use a Provider component to scope atom state to a React subtree - useful for isolated widget instances or test isolation.

Can Jotai atoms be async?

Yes. An atom can return a Promise, making it an async atom. Reading an async atom suspends the component until the promise resolves. Wrap the consuming component in a React Suspense boundary to handle the loading state.

What is atomFamily in Jotai?

atomFamily creates a parameterised set of atoms - one atom per unique parameter value. It is useful for per-item state, such as a separate completed flag for each todo item identified by its ID.

What is the difference between useAtom, useAtomValue, and useSetAtom?

useAtom returns both value and setter and re-renders on changes. useAtomValue returns only the value and re-renders on changes. useSetAtom returns only the setter and never re-renders when the atom changes. Use the most restrictive hook to minimise unnecessary renders.