mrkeyoor.com_
Sat 08 Aug 21:01 UTC
npmWeb Frontendupdated 08 Aug 2026

jotai

Jotai is an atomic state library for React. State lives in small atom objects rather than one central store: primitive atoms hold values, derived atoms compute from other atoms, and writable atoms model actions. Components subscribe through useAtom, useAtomValue, or useSetAtom, so updates can rerender only the consumers that read the affected atom. A vanilla store API works outside React, while utilities cover storage, families, selection, splitting, reset, hydration, and async values.

Verdict

Jotai is a strong fit when React state naturally decomposes into a graph of small values and computations. Skip it for ordinary local state or when your team needs the centralized events and operational conventions Redux supplies.

API stability4/5Jotai 2 retains the small atom, useAtom, useAtomValue, useSetAtom, Provider, and createStore core established for the current major. Utilities are broader and some are documented as escape hatches with stability caveats, while migration from version 1 changed async write semantics and store APIs. Core usage is predictable, but advanced utility consumers should read release notes.
Docs4/5jotai.org has separate core, utility, integration, guide, recipe, extension, and migration sections with working examples for SSR, storage, testing, TypeScript, React Native, Next.js, async state, and vanilla stores. It openly marks some guidance as outdated or unpublished, and several advanced pages require stitching concepts together, so the breadth is better than the consistency.
Maintenance5/5Version 2.20.2 was published in July 2026 and the repository was pushed in August 2026. The GitHub snapshot shows only four open issues and pull requests despite more than 21,000 stars, plus tests cover React and vanilla utilities across many cases. The active pmndrs organization and frequent current-major releases indicate sustained maintenance.
Ecosystem5/5The package supplies React, vanilla, utility, Babel, SWC, and React Native entry points, and its documentation includes Next.js, Remix, Waku, Vite, testing, storage, observables, Immer, XState, URQL, TanStack Query, and other extensions. Millions of weekly downloads and a large pmndrs community make examples easy to find, though extensions vary in ownership and maturity.

Use it if

  • You have shared React state whose dependency graph is easier to express as small composable atoms than reducers or store slices
  • You want derived and async state to track dependencies automatically as atoms read other atoms
  • You need component-level subscriptions without selectors for every ordinary state read
  • You want the same atoms accessed through React hooks and a small imperative store outside React
Skip it if

Setup reality

npm install jotai is the entire package install. React is an optional peer in the package metadata because the vanilla entry can run without it, but React hook usage needs React 17 or newer. TypeScript declarations and CommonJS, ESM, and React Native export conditions ship in the package; there is no provider or generated code required for a client-only app. The first real trap is identity: an atom is a configuration object, and recreating it on every render creates new state and can loop. Define atoms at module scope or memoize dynamic ones. Provider-less mode uses a default store. That is convenient in the browser but unsafe for request-scoped server rendering, where one global store may outlive a request, so put Provider around each app request or subtree. Hydrate server values with useHydrateAtoms inside that provider, and remember an atom is normally hydrated once per store. Async read atoms return promises and rely on React Suspense behavior; server rendering cannot simply return unresolved promises in every framework path. Storage utilities need client-safe access. Next.js still prerenders client components, so createJSONStorage must guard window, and UI that changes after localStorage loads may need a client-only boundary to prevent hydration mismatch. Utilities come from jotai/utils, while store-only code can import jotai/vanilla. Large object atoms can still rerender too much; split state, use selectAtom with stable selectors, or use focused derived atoms after measuring. Jotai does not prescribe effects, server-cache invalidation, normalized entities, or action logging, so teams must set those conventions themselves.

Patterns

Create and update a primitive atomcreate-primitive-atom

import { atom, useAtom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return <button onClick={() => setCount((n) => n + 1)}>{count}</button>
}

Define the atom outside render so its object identity stays stable.

Compute a value from other atomsderive-read-only-state

const priceAtom = atom(20)
const quantityAtom = atom(3)
const totalAtom = atom((get) => get(priceAtom) * get(quantityAtom))

function Total() {
  const total = useAtomValue(totalAtom)
  return <output>{total}</output>
}

Dependencies are tracked when the read function calls get and refreshed when it runs again.

Model an action with a write-only atomcreate-write-action

const countAtom = atom(0)
const addAtom = atom(null, (_get, set, amount) => {
  set(countAtom, (value) => value + amount)
})

function AddFive() {
  const add = useSetAtom(addAtom)
  return <button onClick={() => add(5)}>Add five</button>
}

useSetAtom avoids subscribing this button to countAtom updates it never reads.

Read asynchronous state with Suspenseload-async-atom

const userIdAtom = atom('42')
const userAtom = atom(async (get) => {
  const response = await fetch(`/api/users/${get(userIdAtom)}`)
  if (!response.ok) throw new Error('user request failed')
  return response.json()
})

function UserName() {
  const user = useAtomValue(userAtom)
  return <span>{user.name}</span>
}

Render this under Suspense and an error boundary; the atom reruns when userIdAtom changes.

Persist an atom to localStoragepersist-browser-state

import { atomWithStorage } from 'jotai/utils'

const themeAtom = atomWithStorage('theme', 'system')

function ThemePicker() {
  const [theme, setTheme] = useAtom(themeAtom)
  return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme}</button>
}

On SSR, the initial server value can differ from localStorage and cause a hydration mismatch.

Scope state with a Providerscope-server-store

import { Provider } from 'jotai'

export function AppState({ children }) {
  return <Provider>{children}</Provider>
}

Use a request-scoped Provider for SSR instead of letting different requests share the default global store.

Hydrate atoms inside a providerhydrate-server-values

import { Provider } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'

function Hydrate({ initialUser, children }) {
  useHydrateAtoms([[userAtom, initialUser]])
  return children
}

<Provider><Hydrate initialUser={user}>{children}</Hydrate></Provider>

An atom is normally hydrated only once for a store; remounting with a different value does not automatically overwrite it.

Read and write atoms outside Reactuse-store-outside-react

import { atom, createStore, Provider } from 'jotai'

const onlineAtom = atom(false)
const store = createStore()
const unsubscribe = store.sub(onlineAtom, () => console.log(store.get(onlineAtom)))
store.set(onlineAtom, true)

// <Provider store={store}><App /></Provider>
unsubscribe()

Components must use the same store through Provider if imperative updates should reach them.

Create resettable statereset-atom-value

import { useAtom } from 'jotai'
import { atomWithReset, RESET } from 'jotai/utils'

const filterAtom = atomWithReset('all')
const [filter, setFilter] = useAtom(filterAtom)
setFilter('open')
setFilter(RESET)

RESET is a special symbol exported by jotai/utils and restores the atom's original value.

Create a prop-dependent atom safelymemoize-dynamic-atom

function Item({ id }) {
  const itemAtom = useMemo(
    () => atom((get) => get(itemsAtom).find((item) => item.id === id)),
    [id],
  )
  const item = useAtomValue(itemAtom)
  return <span>{item?.name}</span>
}

Calling atom directly on every render creates a different config and can trigger an infinite update loop.

Alternatives

PackageRegistryPick it when
zustandnpmYou prefer one small hook-based store with actions and selectors instead of an atom graph
valtionpmYou want mutable proxy state with automatic snapshot subscriptions
@reduxjs/toolkitnpmYou need reducers, middleware, event visibility, established team conventions, and a large integration ecosystem