# Jotai
Jotai is a primitive and flexible state management library for React that takes an atomic approach to global state management. It scales from a simple `useState` replacement to enterprise applications with complex requirements. With a minimal core API of just 2kb, Jotai provides an intuitive way to manage state using atoms - small units of state that can be combined and composed to build complex state logic.
The library is TypeScript-oriented and works seamlessly with Next.js, Remix, Waku, and React Native. Unlike other state management solutions, Jotai doesn't use string keys (like Recoil) and leverages React Suspense for handling asynchronous operations natively. Atoms are created outside of components, can be composed into derived atoms, and automatically track dependencies for optimal re-renders.
## Core API
### atom - Create State Atoms
The `atom` function creates an atom config that represents a piece of state. Atoms can be primitive (holding a simple value), derived read-only (computed from other atoms), derived write-only (action atoms), or read-write derived atoms.
```tsx
import { atom } from 'jotai'
// Primitive atoms - hold simple values
const countAtom = atom(0)
const nameAtom = atom('John')
const todosAtom = atom([{ id: 1, text: 'Learn Jotai', done: false }])
// Read-only derived atom - computed from other atoms
const doubledCountAtom = atom((get) => get(countAtom) * 2)
// Write-only atom - action to modify state
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1)
})
// Read-write derived atom - both computed value and setter
const countWithActionsAtom = atom(
(get) => get(countAtom),
(get, set, action: 'increment' | 'decrement') => {
const current = get(countAtom)
set(countAtom, action === 'increment' ? current + 1 : current - 1)
}
)
// Async derived atom - fetches data reactively
const userIdAtom = atom(1)
const userAtom = atom(async (get, { signal }) => {
const userId = get(userIdAtom)
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ signal }
)
return response.json()
})
// Using onMount for side effects when atom is first subscribed
const wsAtom = atom(null)
wsAtom.onMount = (setAtom) => {
const ws = new WebSocket('wss://example.com')
ws.onmessage = (e) => setAtom(JSON.parse(e.data))
return () => ws.close() // cleanup on unmount
}
```
### useAtom - Read and Write Atoms
The `useAtom` hook reads an atom's value and returns an update function, similar to React's `useState`. It automatically subscribes to atom changes and triggers re-renders when the atom value updates.
```tsx
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
Count: {count}
)
}
// Creating atoms dynamically (must be memoized)
function DynamicAtomExample({ initialValue }: { initialValue: number }) {
const [value, setValue] = useAtom(
useMemo(() => atom(initialValue), [initialValue])
)
return {value}
}
```
### useAtomValue - Read-Only Access
The `useAtomValue` hook provides read-only access to an atom's value. It's useful when you only need to read state without updating it, and provides better performance than destructuring `useAtom`.
```tsx
import { atom, useAtomValue } from 'jotai'
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)
function Display() {
const count = useAtomValue(countAtom)
const doubled = useAtomValue(doubledAtom)
return (
Count: {count}
Doubled: {doubled}
)
}
```
### useSetAtom - Write-Only Access
The `useSetAtom` hook returns only the setter function for an atom. This is useful when you need to update state without subscribing to changes, preventing unnecessary re-renders.
```tsx
import { atom, useSetAtom, useAtomValue } from 'jotai'
const countAtom = atom(0)
// This component won't re-render when countAtom changes
function IncrementButton() {
const setCount = useSetAtom(countAtom)
return
}
// This component only displays and re-renders on changes
function CountDisplay() {
const count = useAtomValue(countAtom)
return {count}
}
function App() {
return (
<>
>
)
}
```
### Provider - Scoped State Container
The `Provider` component creates an isolated state container for a component subtree. Multiple providers can coexist, and nested providers create independent state scopes. Without a Provider, atoms use a default global store.
```tsx
import { atom, useAtom, Provider, createStore } from 'jotai'
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
return
}
// Each Provider has independent state
function App() {
return (
{/* This counter is independent */}
{/* This counter is also independent */}
)
}
// Using a custom store
const myStore = createStore()
myStore.set(countAtom, 10) // Pre-set value
function AppWithStore() {
return (
{/* Starts at 10 */}
)
}
```
### createStore - External Store Management
The `createStore` function creates a store that can be used both inside and outside React components. It provides `get`, `set`, and `sub` methods for reading, writing, and subscribing to atom changes.
```tsx
import { atom, createStore, Provider, useAtomValue } from 'jotai'
const countAtom = atom(0)
const store = createStore()
// Use store outside React
store.set(countAtom, 10)
console.log(store.get(countAtom)) // 10
// Subscribe to changes
const unsub = store.sub(countAtom, () => {
console.log('Count changed:', store.get(countAtom))
})
// Update from anywhere (e.g., WebSocket handler, timer)
setInterval(() => {
store.set(countAtom, (prev) => prev + 1)
}, 1000)
// Use same store in React
function App() {
return (
)
}
function CountDisplay() {
const count = useAtomValue(countAtom)
return
{count}
}
```
## Utilities (jotai/utils)
### atomWithStorage - Persistent State
The `atomWithStorage` utility creates an atom that automatically persists to localStorage, sessionStorage, or AsyncStorage (React Native). It includes cross-tab synchronization for web applications.
```tsx
import { useAtom } from 'jotai'
import { atomWithStorage, createJSONStorage, RESET } from 'jotai/utils'
import AsyncStorage from '@react-native-async-storage/async-storage'
// Basic localStorage persistence
const darkModeAtom = atomWithStorage('darkMode', false)
// With sessionStorage
const sessionStorage = createJSONStorage(() => globalThis.sessionStorage)
const sessionAtom = atomWithStorage('session', null, sessionStorage)
// React Native with AsyncStorage
const rnStorage = createJSONStorage(() => AsyncStorage)
const userPrefsAtom = atomWithStorage('userPrefs', {}, rnStorage)
// With getOnInit for immediate stored value
const themeAtom = atomWithStorage('theme', 'light', undefined, {
getOnInit: true, // Returns stored value immediately, not initialValue
})
function Settings() {
const [darkMode, setDarkMode] = useAtom(darkModeAtom)
return (
{/* Reset to initial value and remove from storage */}
)
}
```
### atomWithReset - Resettable Atoms
The `atomWithReset` utility creates atoms that can be reset to their initial value using the `RESET` symbol or `useResetAtom` hook.
```tsx
import { useAtom } from 'jotai'
import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'
const counterAtom = atomWithReset(0)
const formAtom = atomWithReset({ name: '', email: '' })
function Counter() {
const [count, setCount] = useAtom(counterAtom)
const resetCount = useResetAtom(counterAtom)
return (
{count}
{/* Alternative: setCount(RESET) */}
)
}
function Form() {
const [form, setForm] = useAtom(formAtom)
return (
)
}
```
### atomWithDefault - Dynamic Default Values
The `atomWithDefault` utility creates a resettable atom whose default value is computed from other atoms. The computed default is used until the atom is explicitly set.
```tsx
import { atom, useAtom } from 'jotai'
import { atomWithDefault, useResetAtom, RESET } from 'jotai/utils'
const baseCountAtom = atom(1)
const derivedCountAtom = atomWithDefault((get) => get(baseCountAtom) * 2)
function Example() {
const [baseCount, setBaseCount] = useAtom(baseCountAtom)
const [derivedCount, setDerivedCount] = useAtom(derivedCountAtom)
const resetDerived = useResetAtom(derivedCountAtom)
return (
Base: {baseCount}
Derived: {derivedCount}
)
}
```
### atomWithRefresh - Refreshable Async Atoms
The `atomWithRefresh` utility creates an atom that can be manually refreshed, forcing the read function to re-evaluate. This is useful for implementing "pull to refresh" functionality.
```tsx
import { useAtom } from 'jotai'
import { atomWithRefresh } from 'jotai/utils'
const postsAtom = atomWithRefresh(async (get) => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return response.json()
})
function PostsList() {
const [posts, refreshPosts] = useAtom(postsAtom)
return (
)
}
```
### loadable - Non-Suspending Async
The `loadable` utility wraps an async atom to provide loading/error states without using React Suspense. It returns an object with `state`, `data`, and `error` properties.
```tsx
import { atom, useAtom } from 'jotai'
import { loadable } from 'jotai/utils'
const asyncAtom = atom(async () => {
const response = await fetch('https://api.example.com/data')
if (!response.ok) throw new Error('Failed to fetch')
return response.json()
})
const loadableAtom = loadable(asyncAtom)
function DataDisplay() {
const [state] = useAtom(loadableAtom)
if (state.state === 'loading') {
return
Loading...
}
if (state.state === 'hasError') {
return
Error: {state.error.message}
}
// state.state === 'hasData'
return
Data: {JSON.stringify(state.data)}
}
// No Suspense boundary needed!
function App() {
return
}
```
### unwrap - Simplified Async Unwrapping
The `unwrap` utility converts an async atom to a sync atom with a fallback value during loading. Unlike `loadable`, errors are thrown rather than captured.
```tsx
import { atom, useAtomValue } from 'jotai'
import { unwrap } from 'jotai/utils'
const countAtom = atom(0)
const delayedCountAtom = atom(async (get) => {
await new Promise((r) => setTimeout(r, 1000))
return get(countAtom)
})
// Returns undefined while loading
const simpleUnwrappedAtom = unwrap(delayedCountAtom)
// Returns 0 initially, then keeps previous value while refreshing
const unwrappedWithFallbackAtom = unwrap(
delayedCountAtom,
(prev) => prev ?? 0
)
function Display() {
const value = useAtomValue(unwrappedWithFallbackAtom)
return
Value: {value}
}
```
### atomFamily - Parameterized Atoms
The `atomFamily` utility creates a function that returns atoms based on parameters, with caching to ensure the same parameters return the same atom instance.
```tsx
import { atom, useAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// Simple parameterized atom
const todoAtomFamily = atomFamily((id: number) =>
atom({ id, text: '', completed: false })
)
// With derived state
const userAtomFamily = atomFamily((userId: string) =>
atom(async () => {
const response = await fetch(`/api/users/${userId}`)
return response.json()
})
)
// With custom equality (for object params)
import deepEqual from 'fast-deep-equal'
const queryAtomFamily = atomFamily(
(params: { page: number; filter: string }) => atom(params),
deepEqual
)
function TodoItem({ id }: { id: number }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id))
return (
)
}
// Memory management - remove unused atoms
todoAtomFamily.remove(5) // Remove atom for id=5
// Auto-cleanup old atoms
todoAtomFamily.setShouldRemove((createdAt, param) => {
return Date.now() - createdAt > 60 * 60 * 1000 // Remove after 1 hour
})
```
### atomWithLazy - Deferred Initialization
The `atomWithLazy` utility creates a primitive atom whose initial value is computed lazily on first use, useful for expensive initialization that should be deferred.
```tsx
import { useAtom } from 'jotai'
import { atomWithLazy } from 'jotai/utils'
// Expensive initialization deferred until first use
const imageDataAtom = atomWithLazy(() => {
console.log('Initializing expensive image data...')
return generateExpensiveImageData()
})
function ImageEditor() {
// Initialization happens here, not at import time
const [imageData, setImageData] = useAtom(imageDataAtom)
return
}
function Home() {
// imageDataAtom is not initialized if user never visits ImageEditor
return
Welcome!
}
```
### useHydrateAtoms - SSR Hydration
The `useHydrateAtoms` hook sets initial values for atoms during hydration in SSR applications. Values are only set once per store to prevent overwriting client-side state.
```tsx
import { atom, useAtom, Provider } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
const countAtom = atom(0)
const userAtom = atom(null)
// Custom provider for testing/SSR
function HydrateAtoms({
initialValues,
children,
}: {
initialValues: Iterable, unknown]>
children: React.ReactNode
}) {
useHydrateAtoms(initialValues)
return children
}
// Usage in Next.js page
function Page({ serverData }: { serverData: { count: number; user: User } }) {
return (
)
}
// For testing with injected values
function TestProvider({
initialValues,
children,
}: {
initialValues: Map, unknown>
children: React.ReactNode
}) {
return (
{children}
)
}
```
### useAtomCallback - Imperative Atom Access
The `useAtomCallback` hook provides imperative access to atoms for use cases like form submission, event handlers, or integration with non-React code.
```tsx
import { atom, useAtom } from 'jotai'
import { useAtomCallback } from 'jotai/utils'
import { useCallback } from 'react'
const countAtom = atom(0)
const userAtom = atom({ name: 'John', email: 'john@example.com' })
function FormWithSubmit() {
const [user, setUser] = useAtom(userAtom)
const submitForm = useAtomCallback(
useCallback(async (get, set) => {
const currentUser = get(userAtom)
const count = get(countAtom)
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ user: currentUser, count }),
})
if (response.ok) {
set(countAtom, 0) // Reset count after submission
}
return response.json()
}, [])
)
return (
)
}
```
### splitAtom - Array Item Atoms
The `splitAtom` utility transforms an atom containing an array into an atom containing an array of atoms, one for each item. This enables efficient individual item updates.
```tsx
import { atom, useAtom, PrimitiveAtom } from 'jotai'
import { splitAtom } from 'jotai/utils'
interface Todo {
id: number
text: string
done: boolean
}
const todosAtom = atom([
{ id: 1, text: 'Learn Jotai', done: false },
{ id: 2, text: 'Build app', done: false },
])
const todoAtomsAtom = splitAtom(todosAtom)
function TodoItem({ todoAtom }: { todoAtom: PrimitiveAtom }) {
const [todo, setTodo] = useAtom(todoAtom)
return (
)
}
```
### atomWithReducer - Reducer Pattern
The `atomWithReducer` utility creates an atom that uses a reducer function for state updates, similar to React's `useReducer`.
```tsx
import { useAtom } from 'jotai'
import { atomWithReducer } from 'jotai/utils'
type CounterAction = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }
const counterReducer = (state: number, action: CounterAction): number => {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return 0
default:
return state
}
}
const counterAtom = atomWithReducer(0, counterReducer)
function Counter() {
const [count, dispatch] = useAtom(counterAtom)
return (
{count}
)
}
```
### atomWithObservable - RxJS Integration
The `atomWithObservable` utility creates an atom from an RxJS Observable or Subject, enabling reactive streams integration with Jotai state.
```tsx
import { useAtom } from 'jotai'
import { atomWithObservable } from 'jotai/utils'
import { interval, BehaviorSubject } from 'rxjs'
import { map } from 'rxjs/operators'
// From interval observable
const timerAtom = atomWithObservable(() =>
interval(1000).pipe(map((i) => `Tick: ${i}`))
)
// From BehaviorSubject (read/write)
const subject = new BehaviorSubject(0)
const subjectAtom = atomWithObservable(() => subject)
// With initial value (avoids suspense)
const timerWithInitialAtom = atomWithObservable(
() => interval(1000).pipe(map((i) => i)),
{ initialValue: 0 }
)
function Timer() {
const [time] = useAtom(timerWithInitialAtom)
return
Time: {time}
}
```
### selectAtom - Derived Slice with Equality
The `selectAtom` utility creates a derived atom that only updates when a selected slice of the source atom changes, using custom equality comparison.
```tsx
import { atom, useAtom } from 'jotai'
import { selectAtom } from 'jotai/utils'
import { useMemo, useCallback } from 'react'
import deepEqual from 'fast-deep-equal'
const userAtom = atom({
name: { first: 'John', last: 'Doe' },
address: { city: 'NYC', zip: '10001' },
preferences: { theme: 'dark' },
})
// Select only the name - updates when name object changes
const nameAtom = selectAtom(userAtom, (user) => user.name)
// With deep equality - only updates when actual values change
const addressAtom = selectAtom(
userAtom,
(user) => user.address,
deepEqual
)
function NameDisplay() {
// Must use stable selector reference
const nameAtom = useMemo(
() => selectAtom(userAtom, (user) => user.name),
[]
)
const [name] = useAtom(nameAtom)
return
{name.first} {name.last}
}
```
## Async Patterns
### Suspense with Async Atoms
Jotai leverages React Suspense for async atoms. When an async atom's promise is pending, components using it will suspend until the promise resolves.
```tsx
import { Suspense } from 'react'
import { atom, useAtom } from 'jotai'
const userIdAtom = atom(1)
const userAtom = atom(async (get, { signal }) => {
const id = get(userIdAtom)
const res = await fetch(`https://api.example.com/users/${id}`, { signal })
return res.json()
})
// Dependent async atom
const userPostsAtom = atom(async (get) => {
const user = await get(userAtom) // Must await in dependent atoms
const res = await fetch(`https://api.example.com/users/${user.id}/posts`)
return res.json()
})
function UserProfile() {
const [user] = useAtom(userAtom)
return
{user.name}
}
function App() {
return (
Loading user...}>
Loading posts...}>
)
}
```
### Async Write Actions
Write functions can be async for operations like API calls. The returned promise can be awaited by the caller.
```tsx
import { atom, useAtom } from 'jotai'
const todosAtom = atom([])
const addTodoAtom = atom(null, async (get, set, text: string) => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
})
const newTodo = await response.json()
set(todosAtom, [...get(todosAtom), newTodo])
return newTodo
})
function AddTodo() {
const [, addTodo] = useAtom(addTodoAtom)
const [text, setText] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async () => {
setIsLoading(true)
try {
await addTodo(text)
setText('')
} finally {
setIsLoading(false)
}
}
return (
setText(e.target.value)} />
)
}
```
## Testing
### Testing Components with Atoms
Test components using atoms by treating Jotai as an implementation detail. Use Provider with `useHydrateAtoms` to inject test values.
```tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Provider, atom, useAtom } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
{count}
)
}
// Test helper
function TestProvider({
initialValues,
children,
}: {
initialValues: [Atom, unknown][]
children: React.ReactNode
}) {
return (
{children}
)
}
function HydrateAtoms({ initialValues, children }) {
useHydrateAtoms(initialValues)
return children
}
// Tests
test('counter increments', async () => {
render()
expect(screen.getByTestId('count')).toHaveTextContent('0')
await userEvent.click(screen.getByText('Increment'))
expect(screen.getByTestId('count')).toHaveTextContent('1')
})
test('counter with initial value', async () => {
render(
)
expect(screen.getByTestId('count')).toHaveTextContent('100')
})
```
## Summary
Jotai provides a powerful yet minimal approach to React state management through its atomic model. The core API consists of just four main exports (`atom`, `useAtom`, `Provider`, `createStore`) that handle most use cases, while the utilities package (`jotai/utils`) offers specialized atoms for persistence, async handling, collections, and SSR. The composable nature of atoms allows building complex state from simple primitives while maintaining fine-grained reactivity and optimal re-rendering.
Common integration patterns include using `atomWithStorage` for persistent user preferences, `atomFamily` for dynamic collections, `loadable` or Suspense for async data fetching, and `createStore` for accessing state outside React components. For SSR frameworks like Next.js, `useHydrateAtoms` enables server-side data to initialize client-side atoms. Testing is straightforward by treating atoms as implementation details and using Provider with hydration for injecting test values.