Zustand vs Jotai: React State Management Compared
AI TOOLS March 10, 2026, 5:30 p.m.

Zustand vs Jotai: React State Management Compared

Zustand and Jotai are two of the most buzz‑worthy state‑management libraries in the React ecosystem today. Both aim to replace Redux‑style boilerplate while staying lightweight and idiomatic. In this article we’ll break down their core philosophies, compare APIs, and see how they perform in real‑world scenarios. By the end you’ll know which tool fits your project’s needs and how to get the most out of either library.

What Is Zustand?

Zustand, which means “state” in German, was created by the team behind react‑spring. It embraces a minimal API built on plain JavaScript objects and functions, letting you define a store with a single create call. The store is a mutable object that can be read directly or via selector hooks, eliminating the need for reducers or actions.

Because Zustand stores are just JavaScript objects, you can use any data structure—arrays, maps, or even class instances—without extra adapters. This flexibility makes it a natural fit for both simple UI flags and complex domain models.

Basic Counter Example

import create from 'zustand'

// Define the store
const useCounterStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 }))
}))

// Component using the store
function Counter() {
  const { count, increment, decrement } = useCounterStore()
  return (
    <div>
      <h3>Zustand Counter</h3>
      <p>Current: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

This example shows how a store can be defined and consumed with a single hook. No context providers, no extra boilerplate—just a plain function returning state and mutators.

What Is Jotai?

Jotai, which translates to “state” in Japanese, takes a different route by embracing the atomic model. Each piece of state is an atom, a tiny isolated unit that can be read and written independently. Components subscribe to atoms via the useAtom hook, and derived atoms let you compute values on the fly.

The atomic approach encourages fine‑grained re‑renders: a component only updates when the atom it subscribes to changes. This can lead to performance gains in large applications where only a subset of UI needs to react to a mutation.

Basic Counter with Jotai

import { atom, useAtom } from 'jotai'

// Define atoms
const countAtom = atom(0)
const incrementAtom = atom(
  get => get(countAtom),
  (get, set, _ = null) => set(countAtom, get(countAtom) + 1)
)
const decrementAtom = atom(
  get => get(countAtom),
  (get, set, _ = null) => set(countAtom, get(countAtom) - 1)
)

// Component using atoms
function Counter() {
  const [count] = useAtom(countAtom)
  const [, increment] = useAtom(incrementAtom)
  const [, decrement] = useAtom(decrementAtom)

  return (
    <div>
      <h3>Jotai Counter</h3>
      <p>Current: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

Even though the code looks a bit longer, the atomic model shines when you need derived or async state, as we’ll explore later.

Core API Comparison

Both libraries expose a hook‑based API, but the mental model differs. Zustand’s create returns a hook that gives you the whole store or a selected slice. Jotai’s atom creates independent units that you combine with useAtom.

  • Store Definition: Zustand uses a single function; Jotai uses multiple atom declarations.
  • Selectors: Zustand supports selector functions for granular reads; Jotai achieves the same with separate atoms.
  • Mutations: Zustand’s set receives a callback to produce a new state; Jotai’s writeable atom receives a set function directly.
  • Async Logic: Both support async, but Jotai’s atom can return a Promise and automatically handle loading states.

Choosing between them often comes down to whether you prefer a monolithic store (Zustand) or a composable, atom‑centric design (Jotai).

Performance & Bundle Size

Zustand’s core is under 2 KB gzipped, and because it relies on a single store, the runtime overhead is minimal. Jotai is similarly tiny—around 1.5 KB gzipped—but its atom graph can introduce a tiny bookkeeping cost for each atom.

In benchmarks, both libraries achieve near‑instant updates for simple counters. The real difference appears in large apps with many independent pieces of state. Jotai’s fine‑grained subscriptions can reduce unnecessary renders, while Zustand’s selector‑based approach can achieve comparable results if you design selectors wisely.

Pro tip: When using Zustand, wrap selector calls in useCallback to avoid re‑creating selector functions on every render. This keeps memoization stable and prevents extra renders.

Real‑World Use Case: Todo List

Let’s build a small Todo list with both libraries to see how they differ in practice. The UI includes adding, toggling, and filtering tasks.

Zustand Todo Store

import create from 'zustand'

const useTodoStore = create(set => ({
  todos: [],
  addTodo: text => set(state => ({
    todos: [...state.todos, { id: Date.now(), text, completed: false }]
  })),
  toggleTodo: id => set(state => ({
    todos: state.todos.map(t =>
      t.id === id ? { ...t, completed: !t.completed } : t
    )
  })),
  // Selector for filtered view
  filteredTodos: filter => get => {
    const all = get().todos
    if (filter === 'active') return all.filter(t => !t.completed)
    if (filter === 'completed') return all.filter(t => t.completed)
    return all
  }
}))

The store packs all logic into one place. The filteredTodos selector demonstrates how you can compute derived data without extra atoms.

Jotai Todo Atoms

import { atom, useAtom } from 'jotai'

// Base atom
const todosAtom = atom([])

// Writeable atoms
const addTodoAtom = atom(
  null,
  (get, set, text) => {
    const newTodo = { id: Date.now(), text, completed: false }
    set(todosAtom, [...get(todosAtom), newTodo])
  }
)

const toggleTodoAtom = atom(
  null,
  (get, set, id) => {
    set(todosAtom,
      get(todosAtom).map(t =>
        t.id === id ? { ...t, completed: !t.completed } : t
      )
    )
  }
)

// Derived atom for filtering
const filteredTodosAtom = filter => atom(
  get => {
    const all = get(todosAtom)
    if (filter === 'active') return all.filter(t => !t.completed)
    if (filter === 'completed') return all.filter(t => t.completed)
    return all
  }
)

Here each piece of behavior is its own atom. The UI can subscribe only to filteredTodosAtom('active') or filteredTodosAtom('completed') without pulling in the whole list.

When to Choose Zustand

  • You prefer a single store that feels like a global object.
  • Your app has moderate state complexity and you want to keep the learning curve low.
  • You need straightforward integration with existing Redux‑style tooling (e.g., devtools).
  • You value simple TypeScript inference—Zustand’s store type is inferred automatically.

Because Zustand’s API mirrors plain JavaScript, onboarding junior developers is usually painless. It also works nicely with server‑side rendering (SSR) as the store can be instantiated per request.

When to Choose Jotai

  • You want atomic, fine‑grained reactivity for large component trees.
  • Your state includes many derived or async values that benefit from composable atoms.
  • You enjoy the functional‑programming vibe of building small, reusable atoms.
  • You need built‑in suspense support for async atoms without extra libraries.

Jotai shines in dashboards, data‑heavy pages, or when you want to isolate state per feature without a monolithic store.

Migration Tips

If you’re moving from Redux to either library, start by identifying slices of state that can become independent atoms (Jotai) or store sections (Zustand). Keep actions as pure functions; they translate directly into set callbacks or writeable atoms.

For a gradual migration, you can run both libraries side‑by‑side. Render a component that reads from Redux and writes to Zustand/Jotai, then slowly replace the Redux consumers.

Pro tip: Use useStore from Zustand with a selector that returns only the fields you need. This mirrors Jotai’s atom granularity while keeping the single‑store model.

Advanced Patterns

Both libraries support middleware or plugins. Zustand offers persist, devtools, and immer out of the box. Jotai provides atomWithStorage, atomWithReset, and atomFamily for dynamic atom creation.

Here’s a quick example of persisting a Zustand store to localStorage:

import { persist } from 'zustand/middleware'

const usePersistedStore = create(persist(
  set => ({
    theme: 'light',
    toggleTheme: () => set(state => ({
      theme: state.theme === 'light' ? 'dark' : 'light'
    }))
  }),
  { name: 'app-theme' } // key in localStorage
))

And the equivalent Jotai pattern using atomWithStorage:

import { atomWithStorage } from 'jotai/utils'

const themeAtom = atomWithStorage('app-theme', 'light')
const toggleThemeAtom = atom(
  get => get(themeAtom),
  (get, set) => set(themeAtom, get(themeAtom) === 'light' ? 'dark' : 'light')
)

Both snippets achieve the same persistence goal, but Jotai’s utility atom makes the intent explicit.

Testing State Logic

Testing with Zustand is straightforward because the store is just a function. You can import the store hook, call it, and invoke actions directly in a Jest test.

import { renderHook, act } from '@testing-library/react-hooks'
import { useCounterStore } from './counterStore'

test('increments correctly', () => {
  const { result } = renderHook(() => useCounterStore())
  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
})

Jotai tests follow a similar pattern, but you often test atoms in isolation using the Provider component.

import { renderHook, act } from '@testing-library/react-hooks'
import { Provider, useAtom } from 'jotai'
import { countAtom, incrementAtom } from './counterAtoms'

test('Jotai increment works', () => {
  const wrapper = ({ children }) => <Provider>{children}</Provider>
  const { result } = renderHook(() => {
    const [count] = useAtom(countAtom)
    const [, increment] = useAtom(incrementAtom)
    return { count, increment }
  }, { wrapper })

  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
})

Both approaches keep tests fast and deterministic, reinforcing the libraries’ simplicity.

Community & Ecosystem

Zustand enjoys strong backing from the react‑spring team and has a vibrant plugin ecosystem (e.g., zustand/middleware, zustand/shallow). The community contributes many examples for Next.js, React Native, and even vanilla JS.

Jotai, while newer, has quickly grown a dedicated community. The official repo ships utilities for storage, async, and atom families. Several UI libraries (e.g., twin.macro) publish Jotai‑compatible hooks, making integration seamless.

Pro Tips for Scaling

  • Zustand: Split large stores into multiple logical stores and combine them with custom hooks. This keeps each store focused and improves TypeScript readability.
  • Jotai: Use atomFamily to generate parameterized atoms for lists (e.g., per‑item state) without creating a separate atom per item manually.
  • Both: Enable React DevTools and the library’s own devtools extensions to visualize state changes during development.

Pro tip: In a Next.js app, instantiate Zustand stores inside getServerSideProps to avoid state leakage between requests. For Jotai, wrap the page with a fresh Provider per request.

Choosing the Right Tool

If you value a single, mutable store that feels like a plain object, Zustand is the natural choice. It offers a gentle learning curve, excellent TypeScript support, and a familiar Redux‑like devtools experience.

If you prefer composability, fine‑grained reactivity, and built‑in support for async/suspense, Jotai gives you atomic granularity that scales elegantly as your UI grows.

In practice, many teams start with one library and later adopt the other for specific subsystems—Zustand for global UI flags, Jotai for isolated feature modules. The low bundle sizes mean you can even mix them without hurting performance.

Conclusion

Zustand and Jotai both deliver lightweight, hook‑

Share this article