Skip to content

Jotai Reference: Atoms, Derived State, Async Atoms, Storage & vs Zustand

Jotai is an atomic state management library for React. Unlike Zustand (one store with selectors), Jotai splits state into small atoms. Components subscribe to individual atoms and only re-render when that specific atom changes. No context providers needed (unless you want scoped state). The mental model is similar to React’s useState but globally shared. Jotai vs Zustand: Jotai is better for highly granular state (each UI widget has its own atom); Zustand is better for structured domain state (users, cart, auth in one store).

1. atom() & useAtom

Create atoms, useAtom hook, read/write atoms, and atom types
// npm install jotai

import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";

// Primitive atom — like useState but globally shared:
const countAtom  = atom(0);
const nameAtom   = atom("Alice");
const darkAtom   = atom(false);
const listAtom   = atom<string[]>([]);

// useAtom — [value, setter] like useState:
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={(c) => setCount((c) => c - 1)}>-</button>
    </div>
  );
}

// Read-only — no re-render on set:
function Display() {
  const count = useAtomValue(countAtom);   // read only
  return <span>{count}</span>;
}

// Write-only — no re-render on read:
function IncrementButton() {
  const setCount = useSetAtom(countAtom);  // write only
  return <button onClick={() => setCount((c) => c + 1)}>+</button>;
}

// Atom family — parameterized atoms (one per ID):
import { atomFamily } from "jotai/utils";
const todoAtom = atomFamily((id: string) => atom({ id, done: false, text: "" }));

function TodoItem({ id }: { id: string }) {
  const [todo, setTodo] = useAtom(todoAtom(id));  // atom per todo item
  return <div onClick={() => setTodo((t) => ({ ...t, done: !t.done }))}>{todo.text}</div>;
}

2. Derived (Computed) Atoms

Read-only derived atoms, read-write derived atoms, and async atoms
import { atom } from "jotai";

const countAtom      = atom(0);
const multiplierAtom = atom(2);

// Read-only derived atom (computed from other atoms):
const doubledAtom = atom((get) => get(countAtom) * get(multiplierAtom));
// Re-evaluates when countAtom or multiplierAtom changes

const itemsAtom  = atom<Item[]>([]);
const filterAtom = atom<"all" | "done" | "pending">("all");

const filteredAtom = atom((get) => {
  const items  = get(itemsAtom);
  const filter = get(filterAtom);
  if (filter === "done")    return items.filter((i) => i.done);
  if (filter === "pending") return items.filter((i) => !i.done);
  return items;
});

// Read-write derived atom — read from one, write to another:
const uppercaseNameAtom = atom(
  (get) => get(nameAtom).toUpperCase(),     // getter
  (_get, set, value: string) =>             // setter
    set(nameAtom, value.toLowerCase())      // writes to original atom
);

// Async atom — fetches data:
const userAtom = atom(async (get) => {
  const id = get(userIdAtom);
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<User>;
});
// Use with Suspense: <Suspense fallback={<Spinner />}><UserProfile /></Suspense>
// In component:
const user = useAtomValue(userAtom);  // suspends until resolved

3. atomWithStorage, atomWithReset & Utils

Persist atoms to localStorage, reset to initial value, and loadable for async
import { atomWithStorage, atomWithReset, RESET } from "jotai/utils";
import { loadable } from "jotai/utils";

// Persist to localStorage:
const themeAtom = atomWithStorage("theme", "light");
// Reads from localStorage on mount, writes on change
// atomWithStorage("key", initialValue, storage?, options?)

// Custom storage (e.g., sessionStorage):
const sessionAtom = atomWithStorage("session-data", null, sessionStorage);

// atomWithReset — can reset to initial value:
const inputAtom = atomWithReset("");

function SearchInput() {
  const [value, setValue] = useAtom(inputAtom);
  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button onClick={() => setValue(RESET)}>Clear</button>
    </div>
  );
}

// loadable — handles async atoms without Suspense:
const loadableUserAtom = loadable(userAtom);

function UserDisplay() {
  const state = useAtomValue(loadableUserAtom);
  if (state.state === "loading")  return <Spinner />;
  if (state.state === "hasError") return <p>Error: {String(state.error)}</p>;
  return <p>{state.data.name}</p>;  // state.state === "hasData"
}

4. Provider, Scoped Atoms & DevTools

Provider for isolated state, store API for testing, and Jotai DevTools
import { Provider, createStore, useStore } from "jotai";

// By default: atoms are globally shared (no Provider needed)
// Provider creates an isolated scope — useful for testing or multiple instances:

function TestWrapper({ children }) {
  return (
    <Provider>
      {children}   {/* atoms here are isolated from other Providers */}
    </Provider>
  );
}

// createStore — for testing (read/write atoms outside React):
const store = createStore();
store.set(countAtom, 10);
store.get(countAtom);   // 10
store.sub(countAtom, () => console.log("changed"));  // subscribe

// Reset specific atoms in tests:
import { createStore } from "jotai";
let store: ReturnType<typeof createStore>;
beforeEach(() => {
  store = createStore();
  // use <Provider store={store}> in render
});

// Jotai DevTools (browser extension — like Redux DevTools):
import { useAtomsDevtools } from "jotai-devtools";
// In your app root: <AtomsDevtools /> (dev only)

// atomWithObservable — integrate RxJS or other observables:
import { atomWithObservable } from "jotai/utils";
import { interval } from "rxjs";
const clockAtom = atomWithObservable(() => interval(1000));

5. Jotai vs Zustand & Performance Patterns

When to choose Jotai vs Zustand, performance optimization, and Next.js SSR
// Jotai vs Zustand:

// Use JOTAI when:
// - Fine-grained subscriptions (each widget/component has isolated state)
// - Derived state is complex (chains of computed values)
// - Async data per component (async atoms + Suspense)
// - Atom composition feels natural for your data shape
// - State is more "spreadsheet-like" (cells with formulas)

// Use ZUSTAND when:
// - Domain model state (users, cart, auth, settings in one place)
// - Actions + reducers pattern feels natural
// - Middleware needed (persist, devtools, immer)
// - Team familiar with Redux patterns

// Granular re-renders — key Jotai advantage:
// Each useAtom(xAtom) subscribes ONLY to xAtom
// Component with useAtomValue(countAtom) re-renders ONLY when count changes
// Compare: useStore() in Zustand re-renders on ANY store change (fix with selector)

// Next.js SSR (avoid atom state leaking between requests):
// npm install jotai-ssr (or use Provider per request)
import { createStore } from "jotai";
function getServerSideProps() {
  const store = createStore();
  store.set(countAtom, 10);   // server-side init
  return { props: { initialState: store.get(countAtom) } };
}
// Client: hydrate atoms from props inside Provider

// Atom scope — isolate atoms per component instance:
import { ScopeProvider } from "jotai-scope";
// Multiple instances of the same component with isolated state
<ScopeProvider atoms={[countAtom]}><Counter /></ScopeProvider>
<ScopeProvider atoms={[countAtom]}><Counter /></ScopeProvider>
// Each Counter has its own countAtom instance

Track Node.js and React ecosystem releases at ReleaseRun. Related: Zustand Reference | TanStack Query Reference | React Reference | React EOL Tracker

🔍 Free tool: npm Package Health Checker — check Jotai and React state management packages for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com