Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
Jotai
https://github.com/pmndrs/jotai
Admin
Jotai is a primitive and flexible state management library for React that scales from simple
...
Tokens:
91,797
Snippets:
873
Trust Score:
9.6
Update:
3 weeks ago
Context
Skills
Chat
Benchmark
82.9
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Jotai Jotai is a primitive and flexible state management library for React. It takes an atomic approach to global React state management, scaling from a simple `useState` replacement to enterprise TypeScript applications with complex requirements. Jotai's core philosophy is based on atoms - small, composable pieces of state that can be combined to create complex state logic. The library is incredibly lightweight with a minimal core API of just 2kb. Unlike other state management solutions that use string keys (like Recoil), Jotai uses object referential identity to track atoms. This makes it TypeScript-friendly and eliminates the need for string-based atom registration. Jotai fully leverages React Suspense for handling asynchronous operations, provides excellent DevTools support, and works seamlessly with React frameworks like Next.js, Remix, Waku, and React Native. ## Core API ### atom - Create Primitive and Derived Atoms The `atom` function creates an atom config that represents a piece of state. Atoms can be primitive (holding a simple value) or derived (computing values from other atoms). The atom config is immutable and doesn't hold a value itself - values exist in stores. ```tsx import { atom } from 'jotai' // Primitive atoms - hold initial values const countAtom = atom(0) const nameAtom = atom('John') const todosAtom = atom([{ id: 1, text: 'Learn Jotai', done: false }]) // Read-only derived atom - computes value from other atoms const doubledCountAtom = atom((get) => get(countAtom) * 2) // Read-write derived atom - can both read and write const countWithActionsAtom = atom( (get) => get(countAtom), (get, set, action: 'increment' | 'decrement') => { const current = get(countAtom) set(countAtom, action === 'increment' ? current + 1 : current - 1) } ) // Write-only atom - null as first argument, only has write function const incrementAtom = atom(null, (get, set) => { set(countAtom, get(countAtom) + 1) }) // Async derived atom - returns a Promise const userAtom = atom(async (get) => { const userId = get(userIdAtom) const response = await fetch(`/api/users/${userId}`) return response.json() }) // Async write atom - async actions const saveUserAtom = atom(null, async (get, set, userData: User) => { const response = await fetch('/api/users', { method: 'POST', body: JSON.stringify(userData), }) const saved = await response.json() set(usersAtom, (prev) => [...prev, saved]) }) // Using onMount for side effects when atom is first subscribed const connectionAtom = atom(null) connectionAtom.onMount = (setAtom) => { const ws = new WebSocket('wss://example.com') ws.onmessage = (e) => setAtom(JSON.parse(e.data)) return () => ws.close() // cleanup on unmount } // Async atom with AbortController signal for cancellation const searchResultsAtom = atom(async (get, { signal }) => { const query = get(searchQueryAtom) const response = await fetch(`/api/search?q=${query}`, { signal }) return response.json() }) ``` ### useAtom - Read and Write Atom Values The `useAtom` hook reads an atom value and returns a tuple with the value and a setter function, similar to React's `useState`. It automatically subscribes to atom updates and re-renders when the atom value changes. ```tsx import { atom, useAtom } from 'jotai' const countAtom = atom(0) const todosAtom = atom<string[]>([]) function Counter() { // Basic usage - returns [value, setValue] tuple const [count, setCount] = useAtom(countAtom) return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount((c) => c + 1)}>Increment (updater)</button> <button onClick={() => setCount(0)}>Reset</button> </div> ) } function TodoList() { const [todos, setTodos] = useAtom(todosAtom) const addTodo = (text: string) => { setTodos((prev) => [...prev, text]) } return ( <div> <ul> {todos.map((todo, i) => ( <li key={i}>{todo}</li> ))} </ul> <button onClick={() => addTodo('New todo')}>Add Todo</button> </div> ) } // Using with derived atoms const derivedAtom = atom( (get) => get(countAtom) * 2, (get, set, newValue: number) => set(countAtom, newValue / 2) ) function DerivedExample() { const [doubled, setDoubled] = useAtom(derivedAtom) return <button onClick={() => setDoubled(10)}>Set to 10 (count becomes 5)</button> } ``` ### useAtomValue - Read-Only Atom Access The `useAtomValue` hook provides read-only access to an atom's value. Use this when you only need to read an atom and don't need to update it, improving performance by avoiding unnecessary re-renders. ```tsx import { atom, useAtomValue } from 'jotai' const countAtom = atom(0) const userAtom = atom({ name: 'John', age: 30 }) function DisplayCount() { // Only reads the value, no setter returned const count = useAtomValue(countAtom) return <span>Count: {count}</span> } function UserProfile() { const user = useAtomValue(userAtom) return ( <div> <p>Name: {user.name}</p> <p>Age: {user.age}</p> </div> ) } // Useful with async atoms (requires Suspense) const dataAtom = atom(async () => { const res = await fetch('/api/data') return res.json() }) function DataDisplay() { const data = useAtomValue(dataAtom) // Suspends until data loads return <pre>{JSON.stringify(data, null, 2)}</pre> } ``` ### useSetAtom - Write-Only Atom Access The `useSetAtom` hook returns only the setter function for an atom. Use this when you need to update an atom but don't need to read its value, which prevents unnecessary re-renders when the atom value changes. ```tsx import { atom, useSetAtom, useAtomValue } from 'jotai' const countAtom = atom(0) // This component never re-renders when countAtom changes function IncrementButton() { const setCount = useSetAtom(countAtom) return ( <button onClick={() => setCount((c) => c + 1)}> Increment </button> ) } // This component re-renders only to display the value function CountDisplay() { const count = useAtomValue(countAtom) return <span>{count}</span> } // Example: Action-only atoms const logoutAtom = atom(null, async (get, set) => { await fetch('/api/logout', { method: 'POST' }) set(userAtom, null) set(tokenAtom, null) }) function LogoutButton() { const logout = useSetAtom(logoutAtom) return <button onClick={logout}>Logout</button> } ``` ### Provider - Scoped State Container The `Provider` component creates an isolated state container for a component subtree. Multiple Providers can be used for different subtrees, enabling state isolation and testing. Without a Provider, atoms use a default global store (provider-less mode). ```tsx import { Provider, atom, useAtom, createStore } from 'jotai' const countAtom = atom(0) function Counter() { const [count, setCount] = useAtom(countAtom) return <button onClick={() => setCount(c => c + 1)}>{count}</button> } // Each Provider has isolated state function App() { return ( <div> {/* These two counters have independent state */} <Provider> <Counter /> {/* Starts at 0 */} </Provider> <Provider> <Counter /> {/* Also starts at 0, independent */} </Provider> </div> ) } // Using a custom store with Provider const myStore = createStore() function AppWithStore() { return ( <Provider store={myStore}> <Counter /> </Provider> ) } // Useful for testing - reset state by remounting Provider function TestableApp() { const [key, setKey] = useState(0) return ( <Provider key={key}> <MyApp /> <button onClick={() => setKey(k => k + 1)}>Reset All State</button> </Provider> ) } ``` ### createStore - Manual Store Management The `createStore` function creates a store for managing atom values outside of React. The store provides `get`, `set`, and `sub` methods for reading, writing, and subscribing to atom changes. ```tsx import { atom, createStore, Provider } from 'jotai' const countAtom = atom(0) const nameAtom = atom('Anonymous') // Create a store const store = createStore() // Read atom value console.log(store.get(countAtom)) // 0 // Set atom value store.set(countAtom, 5) console.log(store.get(countAtom)) // 5 // Subscribe to changes const unsub = store.sub(countAtom, () => { console.log('countAtom changed to:', store.get(countAtom)) }) store.set(countAtom, 10) // Logs: "countAtom changed to: 10" unsub() // Unsubscribe // Use with Provider for React integration function App() { return ( <Provider store={store}> <MyComponent /> </Provider> ) } // Access default store (provider-less mode) import { getDefaultStore } from 'jotai' const defaultStore = getDefaultStore() defaultStore.set(countAtom, 100) ``` ### useStore - Access Current Store The `useStore` hook returns the current store within a Provider context. Useful for imperative operations or integrating with non-React code. ```tsx import { useStore, atom } from 'jotai' const countAtom = atom(0) function AdvancedComponent() { const store = useStore() const handleExternalEvent = () => { // Imperatively read and write atoms const currentCount = store.get(countAtom) store.set(countAtom, currentCount + 1) } useEffect(() => { // Subscribe to atom changes outside of React render const unsub = store.sub(countAtom, () => { console.log('Count changed:', store.get(countAtom)) }) return unsub }, [store]) return <button onClick={handleExternalEvent}>Update</button> } ``` ## Utilities (jotai/utils) ### atomWithStorage - Persistent Storage Creates an atom that persists its value to localStorage, sessionStorage, or AsyncStorage (React Native). Includes automatic cross-tab synchronization. ```tsx import { useAtom } from 'jotai' import { atomWithStorage, createJSONStorage, RESET } from 'jotai/utils' // Basic localStorage persistence const themeAtom = atomWithStorage('theme', 'light') // Session storage const sessionDataAtom = atomWithStorage('session', {}, sessionStorage) // Custom storage with options const settingsAtom = atomWithStorage( 'app-settings', { notifications: true, volume: 80 }, undefined, // use default localStorage { getOnInit: true } // load from storage on initialization ) function ThemeToggle() { const [theme, setTheme] = useAtom(themeAtom) return ( <div> <p>Current theme: {theme}</p> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> {/* Reset to initial value and remove from storage */} <button onClick={() => setTheme(RESET)}>Reset Theme</button> </div> ) } // React Native AsyncStorage import AsyncStorage from '@react-native-async-storage/async-storage' const asyncStorageAtom = atomWithStorage( 'mobile-data', { user: null }, createJSONStorage(() => AsyncStorage) ) // Custom storage with validation using Zod import { z } from 'zod' const userSchema = z.object({ name: z.string(), age: z.number().positive(), }) const validatedUserAtom = atomWithStorage('user', { name: '', age: 0 }, { getItem: (key, initialValue) => { const stored = localStorage.getItem(key) try { return userSchema.parse(JSON.parse(stored ?? '')) } catch { return initialValue } }, setItem: (key, value) => { localStorage.setItem(key, JSON.stringify(value)) }, removeItem: (key) => localStorage.removeItem(key), }) ``` ### atomWithReset - Resettable Atoms Creates an atom that can be reset to its initial value using the `RESET` symbol or `useResetAtom` hook. ```tsx import { useAtom, atom } from 'jotai' import { atomWithReset, useResetAtom, RESET } from 'jotai/utils' const counterAtom = atomWithReset(0) const formAtom = atomWithReset({ name: '', email: '', message: '', }) function Counter() { const [count, setCount] = useAtom(counterAtom) const resetCount = useResetAtom(counterAtom) return ( <div> <p>Count: {count}</p> <button onClick={() => setCount((c) => c + 1)}>Increment</button> <button onClick={resetCount}>Reset (hook)</button> <button onClick={() => setCount(RESET)}>Reset (symbol)</button> </div> ) } function ContactForm() { const [form, setForm] = useAtom(formAtom) const resetForm = useResetAtom(formAtom) return ( <form> <input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} placeholder="Name" /> <input value={form.email} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} placeholder="Email" /> <textarea value={form.message} onChange={(e) => setForm((f) => ({ ...f, message: e.target.value }))} placeholder="Message" /> <button type="button" onClick={resetForm}>Clear Form</button> </form> ) } ``` ### atomWithDefault - Computed Initial Value Creates a resettable atom where the initial value is computed from a read function rather than a static value. ```tsx import { atom, useAtom } from 'jotai' import { atomWithDefault, useResetAtom, RESET } from 'jotai/utils' const baseCountAtom = atom(10) // Initial value is computed from baseCountAtom const derivedCountAtom = atomWithDefault((get) => get(baseCountAtom) * 2) function Example() { const [baseCount, setBaseCount] = useAtom(baseCountAtom) const [derivedCount, setDerivedCount] = useAtom(derivedCountAtom) const resetDerived = useResetAtom(derivedCountAtom) return ( <div> <p>Base: {baseCount}</p> <p>Derived: {derivedCount}</p> <button onClick={() => setBaseCount((c) => c + 1)}> Inc Base (derived follows when not overwritten) </button> <button onClick={() => setDerivedCount(100)}> Set Derived to 100 (disconnects from base) </button> <button onClick={resetDerived}> Reset Derived (reconnects to base) </button> </div> ) } ``` ### atomWithRefresh - Refreshable Async Atoms Creates an atom that can be manually refreshed to re-execute its read function, useful for re-fetching data. ```tsx import { useAtom } from 'jotai' import { atomWithRefresh } from 'jotai/utils' import { Suspense } from 'react' const postsAtom = atomWithRefresh(async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') return response.json() }) function PostsList() { const [posts, refresh] = useAtom(postsAtom) return ( <div> <button onClick={refresh}>Refresh Posts</button> <ul> {posts.map((post: any) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ) } function App() { return ( <Suspense fallback={<div>Loading posts...</div>}> <PostsList /> </Suspense> ) } ``` ### loadable - Non-Suspense Async Handling Wraps an async atom to handle loading and error states without Suspense, returning `{ state, data, error }`. ```tsx import { atom, useAtom } from 'jotai' import { loadable } from 'jotai/utils' const userIdAtom = atom(1) const asyncUserAtom = atom(async (get) => { const id = get(userIdAtom) const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) if (!response.ok) throw new Error('Failed to fetch user') return response.json() }) const loadableUserAtom = loadable(asyncUserAtom) // No Suspense needed! function UserCard() { const [userLoadable] = useAtom(loadableUserAtom) if (userLoadable.state === 'loading') { return <div>Loading user...</div> } if (userLoadable.state === 'hasError') { return <div>Error: {userLoadable.error.message}</div> } // state === 'hasData' const user = userLoadable.data return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ) } ``` ### unwrap - Convert Async to Sync with Fallback Converts an async atom to a sync atom with an optional fallback value during loading. Unlike loadable, errors are thrown rather than handled. ```tsx import { atom, useAtom } from 'jotai' import { unwrap } from 'jotai/utils' const asyncCountAtom = atom(async () => { await new Promise((r) => setTimeout(r, 1000)) return 42 }) // Returns undefined while loading const unwrappedAtom = unwrap(asyncCountAtom) // Returns 0 while loading, then keeps previous value on refetch const unwrappedWithFallbackAtom = unwrap(asyncCountAtom, (prev) => prev ?? 0) function Counter() { const [count] = useAtom(unwrappedWithFallbackAtom) return <div>Count: {count}</div> // Shows 0 initially, then 42 } // Useful for deriving from async atoms const derivedAtom = atom((get) => { const count = get(unwrappedWithFallbackAtom) // Always sync! return count * 2 }) ``` ### atomFamily - Parameterized Atom Factory Creates a function that returns atoms based on parameters. Atoms are cached and reused for the same parameter values. ```tsx import { atom, useAtom } from 'jotai' import { atomFamily } from 'jotai/utils' import deepEqual from 'fast-deep-equal' // Simple parameterized atom const todoAtomFamily = atomFamily((id: number) => atom({ id, text: '', completed: false }) ) // With derived atom const userAtomFamily = atomFamily((userId: number) => atom(async () => { const response = await fetch(`/api/users/${userId}`) return response.json() }) ) // With deep equality for object params const itemAtomFamily = atomFamily( (params: { category: string; id: number }) => atom(params), deepEqual // Compare params deeply ) function TodoItem({ id }: { id: number }) { const [todo, setTodo] = useAtom(todoAtomFamily(id)) return ( <div> <input value={todo.text} onChange={(e) => setTodo({ ...todo, text: e.target.value })} /> <input type="checkbox" checked={todo.completed} onChange={() => setTodo({ ...todo, completed: !todo.completed })} /> </div> ) } // Memory management - remove unused atoms function cleanup() { todoAtomFamily.remove(123) // Remove specific atom // Auto-remove atoms older than 1 hour todoAtomFamily.setShouldRemove((createdAt) => Date.now() - createdAt > 60 * 60 * 1000 ) } ``` ### splitAtom - List Item Atoms Splits an array atom into individual item atoms, enabling efficient updates to specific items without re-rendering the entire list. ```tsx import { atom, useAtom, PrimitiveAtom } from 'jotai' import { splitAtom } from 'jotai/utils' interface Todo { id: number text: string done: boolean } const todosAtom = atom<Todo[]>([ { id: 1, text: 'Learn Jotai', done: false }, { id: 2, text: 'Build app', done: false }, ]) const todoAtomsAtom = splitAtom(todosAtom) function TodoItem({ todoAtom, onRemove }: { todoAtom: PrimitiveAtom<Todo> onRemove: () => void }) { const [todo, setTodo] = useAtom(todoAtom) return ( <div> <input type="checkbox" checked={todo.done} onChange={() => setTodo({ ...todo, done: !todo.done })} /> <input value={todo.text} onChange={(e) => setTodo({ ...todo, text: e.target.value })} /> <button onClick={onRemove}>Delete</button> </div> ) } function TodoList() { const [todoAtoms, dispatch] = useAtom(todoAtomsAtom) return ( <div> {todoAtoms.map((todoAtom) => ( <TodoItem key={`${todoAtom}`} todoAtom={todoAtom} onRemove={() => dispatch({ type: 'remove', atom: todoAtom })} /> ))} <button onClick={() => dispatch({ type: 'insert', value: { id: Date.now(), text: '', done: false }, }) } > Add Todo </button> </div> ) } ``` ### selectAtom - Derived Slice with Equality Creates a derived atom that only updates when a selected slice of the original atom changes, with optional custom equality. ```tsx import { atom, useAtom } from 'jotai' import { selectAtom } from 'jotai/utils' import { useCallback, useMemo } from 'react' import deepEqual from 'fast-deep-equal' const userAtom = atom({ profile: { name: 'John', avatar: '/john.png' }, settings: { theme: 'dark', notifications: true }, stats: { posts: 42, followers: 100 }, }) // Select just the profile - updates only when profile changes const profileAtom = selectAtom(userAtom, (user) => user.profile) // With deep equality - ignores reference changes if data is same const settingsAtom = selectAtom( userAtom, (user) => user.settings, deepEqual ) function ProfileDisplay() { const [profile] = useAtom(profileAtom) return <div>{profile.name}</div> // Won't re-render when settings change } // In component - use useMemo or useCallback for stable references function PostCount() { const postCountAtom = useMemo( () => selectAtom(userAtom, (user) => user.stats.posts), [] ) const [postCount] = useAtom(postCountAtom) return <span>{postCount} posts</span> } ``` ### atomWithReducer - Redux-Style Reducers Creates an atom that updates via a reducer function, similar to React's `useReducer`. ```tsx import { useAtom } from 'jotai' import { atomWithReducer } from 'jotai/utils' type State = { count: number; history: number[] } type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'set'; value: number } | { type: 'reset' } const initialState: State = { count: 0, history: [] } function reducer(state: State, action: Action): State { switch (action.type) { case 'increment': return { count: state.count + 1, history: [...state.history, state.count + 1], } case 'decrement': return { count: state.count - 1, history: [...state.history, state.count - 1], } case 'set': return { count: action.value, history: [...state.history, action.value], } case 'reset': return initialState default: return state } } const counterAtom = atomWithReducer(initialState, reducer) function Counter() { const [state, dispatch] = useAtom(counterAtom) return ( <div> <p>Count: {state.count}</p> <p>History: {state.history.join(', ')}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'set', value: 100 })}>Set 100</button> <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> </div> ) } ``` ### useAtomCallback - Imperative Atom Access Hook for reading and writing atoms imperatively without subscribing to updates. Useful for event handlers and 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', score: 0 }) function GameComponent() { const [count] = useAtom(countAtom) // Read atoms without subscribing const getState = useAtomCallback( useCallback((get) => ({ count: get(countAtom), user: get(userAtom), }), []) ) // Write atoms imperatively const saveScore = useAtomCallback( useCallback((get, set) => { const currentCount = get(countAtom) set(userAtom, (prev) => ({ ...prev, score: prev.score + currentCount })) set(countAtom, 0) }, []) ) // Use in event handlers const handleGameEnd = async () => { const state = getState() await fetch('/api/scores', { method: 'POST', body: JSON.stringify(state), }) saveScore() } return ( <div> <p>Current count: {count}</p> <button onClick={handleGameEnd}>End Game & Save</button> </div> ) } ``` ### useHydrateAtoms - Initialize Atoms with Server Data Hydrates atoms with initial values, typically used for SSR or initializing atoms with server-fetched data. ```tsx import { atom, useAtom } from 'jotai' import { useHydrateAtoms } from 'jotai/utils' const countAtom = atom(0) const userAtom = atom<User | null>(null) // Basic hydration function App({ initialCount, initialUser }: Props) { useHydrateAtoms([ [countAtom, initialCount], [userAtom, initialUser], ]) const [count] = useAtom(countAtom) const [user] = useAtom(userAtom) return ( <div> <p>Count: {count}</p> <p>User: {user?.name}</p> </div> ) } // TypeScript - use Map for type safety function TypeSafeApp({ serverData }: { serverData: ServerData }) { useHydrateAtoms( new Map<any, any>([ [countAtom, serverData.count], [userAtom, serverData.user], ]) ) // ... } // Next.js getServerSideProps example export async function getServerSideProps() { const user = await fetchUser() return { props: { initialUser: user }, } } ``` ### atomWithObservable - RxJS Integration Creates an atom from an RxJS Observable or Subject, with the atom value being the latest emitted value. ```tsx import { useAtom } from 'jotai' import { atomWithObservable } from 'jotai/utils' import { interval, Subject } from 'rxjs' import { map, scan } from 'rxjs/operators' import { Suspense } from 'react' // From interval observable const timerAtom = atomWithObservable(() => interval(1000).pipe(map((i) => `Tick #${i}`)) ) // From Subject for bidirectional communication const messageSubject = new Subject<string>() const messagesAtom = atomWithObservable(() => messageSubject.pipe(scan((acc, msg) => [...acc, msg], [] as string[])) ) // With initial value (no Suspense needed) const counterAtom = atomWithObservable( () => interval(1000), { initialValue: 0 } ) function Timer() { const [tick] = useAtom(timerAtom) return <div>{tick}</div> } function App() { return ( <Suspense fallback="Starting timer..."> <Timer /> </Suspense> ) } // Send messages to the subject function sendMessage(msg: string) { messageSubject.next(msg) } ``` ## Extensions ### jotai-tanstack-query - TanStack Query Integration Integrates TanStack Query (React Query) with Jotai for powerful server state management with caching, background refetching, and more. ```tsx import { atom, useAtom } from 'jotai' import { atomWithQuery, atomWithMutation, atomWithInfiniteQuery, } from 'jotai-tanstack-query' // Basic query atom const userIdAtom = atom(1) const userAtom = atomWithQuery((get) => ({ queryKey: ['users', get(userIdAtom)], queryFn: async ({ queryKey: [, id] }) => { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) return res.json() }, })) function UserProfile() { const [{ data, isPending, isError, refetch }] = useAtom(userAtom) if (isPending) return <div>Loading...</div> if (isError) return <div>Error loading user</div> return ( <div> <h2>{data.name}</h2> <p>{data.email}</p> <button onClick={() => refetch()}>Refresh</button> </div> ) } // Mutation atom const createPostAtom = atomWithMutation(() => ({ mutationKey: ['createPost'], mutationFn: async (newPost: { title: string; body: string }) => { const res = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify(newPost), headers: { 'Content-type': 'application/json' }, }) return res.json() }, })) function CreatePost() { const [{ mutate, isPending, isSuccess, data }] = useAtom(createPostAtom) return ( <div> <button onClick={() => mutate({ title: 'New Post', body: 'Content here' })} disabled={isPending} > {isPending ? 'Creating...' : 'Create Post'} </button> {isSuccess && <p>Created post with ID: {data.id}</p>} </div> ) } // Infinite query for pagination const postsAtom = atomWithInfiniteQuery(() => ({ queryKey: ['posts'], queryFn: async ({ pageParam }) => { const res = await fetch( `https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=10` ) return res.json() }, initialPageParam: 1, getNextPageParam: (lastPage, allPages, lastPageParam) => lastPage.length === 10 ? lastPageParam + 1 : undefined, })) function PostsList() { const [{ data, fetchNextPage, hasNextPage, isFetchingNextPage }] = useAtom(postsAtom) return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.map((post: any) => ( <article key={post.id}>{post.title}</article> ))} </div> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more posts'} </button> </div> ) } ``` ### jotai-immer - Immutable Updates with Immer Enables mutable-style state updates using Immer's `produce` function while maintaining immutability. ```tsx import { useAtom } from 'jotai' import { atomWithImmer, useImmerAtom, withImmer } from 'jotai-immer' import { atom } from 'jotai' interface Todo { id: number text: string completed: boolean } interface State { todos: Todo[] filter: 'all' | 'active' | 'completed' } // Create atom with Immer support const stateAtom = atomWithImmer<State>({ todos: [], filter: 'all', }) function TodoApp() { const [state, setState] = useAtom(stateAtom) const addTodo = (text: string) => { // Mutate the draft directly - Immer handles immutability setState((draft) => { draft.todos.push({ id: Date.now(), text, completed: false, }) }) } const toggleTodo = (id: number) => { setState((draft) => { const todo = draft.todos.find((t) => t.id === id) if (todo) { todo.completed = !todo.completed } }) } const removeTodo = (id: number) => { setState((draft) => { const index = draft.todos.findIndex((t) => t.id === id) if (index !== -1) { draft.todos.splice(index, 1) } }) } return ( <div> {state.todos.map((todo) => ( <div key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> <span>{todo.text}</span> <button onClick={() => removeTodo(todo.id)}>Delete</button> </div> ))} </div> ) } // Or wrap existing atom with Immer const primitiveAtom = atom({ count: 0, items: [] as string[] }) const immerAtom = withImmer(primitiveAtom) // Or use the hook directly function Counter() { const [state, setState] = useImmerAtom(primitiveAtom) return ( <button onClick={() => setState((draft) => { draft.count++ })}> {state.count} </button> ) } ``` ## Summary Jotai provides a powerful yet minimal approach to React state management through its atomic model. The core API (`atom`, `useAtom`, `useAtomValue`, `useSetAtom`, `Provider`, `createStore`) covers most use cases with just a few functions. For common patterns, the utility functions in `jotai/utils` provide ready-made solutions for storage persistence (`atomWithStorage`), resettable state (`atomWithReset`), async handling without Suspense (`loadable`, `unwrap`), parameterized atoms (`atomFamily`), efficient list rendering (`splitAtom`), and more. Integration with the broader React ecosystem is seamless through official extensions. The `jotai-tanstack-query` package brings TanStack Query's powerful server state management to Jotai atoms, while `jotai-immer` enables intuitive mutable-style updates. Jotai's design makes it easy to start simple and incrementally adopt more advanced patterns as needed. The library's TypeScript-first approach ensures type safety throughout, and its minimal bundle size (2kb core) makes it suitable for performance-critical applications. Whether building a small widget or a large-scale application, Jotai's composable atoms provide a solid foundation for managing React state.