Jotai
Primitive and flexible atomic state management for React
Jotai - Primitive and Flexible State
Jotai (Japanese for "state") takes an atomic approach to React state management. Instead of a single store, you create small, independent pieces of state called atoms. Components subscribe only to the atoms they use, providing optimal re-renders out of the box.
Why Jotai?
- Atomic — State split into independent atoms
- Minimal API — Just atom() and useAtom()
- No Providers — Works without wrapping your app
- Derived State — Easy computed values
- TypeScript — Full type inference
Installation
npm install jotai
Basic Atoms
import { atom, useAtom } from 'jotai';
// Create atoms - primitive pieces of state
const countAtom = atom(0);
const nameAtom = atom('');
const darkModeAtom = atom(false);
// Use atoms in components
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
);
}
// Read-only hook for performance
import { useAtomValue, useSetAtom } from 'jotai';
function Display() {
// Only subscribes to read
const count = useAtomValue(countAtom);
return <span>{count}</span>;
}
function Controls() {
// Only gets setter, never re-renders from count changes
const setCount = useSetAtom(countAtom);
return (
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
);
}
Derived Atoms
import { atom, useAtomValue } from 'jotai';
// Primitive atoms
const priceAtom = atom(100);
const quantityAtom = atom(1);
const taxRateAtom = atom(0.08);
// Read-only derived atom
const subtotalAtom = atom((get) => {
const price = get(priceAtom);
const quantity = get(quantityAtom);
return price * quantity;
});
const taxAtom = atom((get) => {
const subtotal = get(subtotalAtom);
const taxRate = get(taxRateAtom);
return subtotal * taxRate;
});
const totalAtom = atom((get) => {
return get(subtotalAtom) + get(taxAtom);
});
// Usage
function PriceSummary() {
const subtotal = useAtomValue(subtotalAtom);
const tax = useAtomValue(taxAtom);
const total = useAtomValue(totalAtom);
return (
<div>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
</div>
);
}
Writable Derived Atoms
import { atom, useAtom } from 'jotai';
const celsiusAtom = atom(25);
// Read-write derived atom
const fahrenheitAtom = atom(
// Getter
(get) => get(celsiusAtom) * 9/5 + 32,
// Setter
(get, set, newFahrenheit) => {
const celsius = (newFahrenheit - 32) * 5/9;
set(celsiusAtom, celsius);
}
);
function TemperatureConverter() {
const [celsius, setCelsius] = useAtom(celsiusAtom);
const [fahrenheit, setFahrenheit] = useAtom(fahrenheitAtom);
return (
<div>
<label>
Celsius:
<input
type="number"
value={celsius}
onChange={(e) => setCelsius(Number(e.target.value))}
/>
</label>
<label>
Fahrenheit:
<input
type="number"
value={fahrenheit}
onChange={(e) => setFahrenheit(Number(e.target.value))}
/>
</label>
</div>
);
}
Todo App Example
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// Atom for the todo list
const todosAtom = atom([]);
// Derived atoms
const todoCountAtom = atom((get) => get(todosAtom).length);
const completedCountAtom = atom((get) =>
get(todosAtom).filter(t => t.completed).length
);
// Action atoms (write-only)
const addTodoAtom = atom(null, (get, set, text) => {
set(todosAtom, (prev) => [...prev, {
id: Date.now(),
text,
completed: false,
}]);
});
const toggleTodoAtom = atom(null, (get, set, id) => {
set(todosAtom, (prev) => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
});
const deleteTodoAtom = atom(null, (get, set, id) => {
set(todosAtom, (prev) => prev.filter(todo => todo.id !== id));
});
// Components
function AddTodo() {
const [text, setText] = useState('');
const addTodo = useSetAtom(addTodoAtom);
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
addTodo(text);
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
function TodoList() {
const todos = useAtomValue(todosAtom);
const toggleTodo = useSetAtom(toggleTodoAtom);
const deleteTodo = useSetAtom(deleteTodoAtom);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>×</button>
</li>
))}
</ul>
);
}
function Stats() {
const total = useAtomValue(todoCountAtom);
const completed = useAtomValue(completedCountAtom);
return <p>{completed} of {total} completed</p>;
}
Async Atoms
import { atom, useAtomValue } from 'jotai';
import { Suspense } from 'react';
// Async atom - returns a promise
const userAtom = atom(async () => {
const response = await fetch('/api/user');
return response.json();
});
// Derived async atom
const userPostsAtom = atom(async (get) => {
const user = await get(userAtom);
const response = await fetch(`/api/users/${user.id}/posts`);
return response.json();
});
// Use with Suspense
function UserProfile() {
const user = useAtomValue(userAtom);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}
Atom with Storage
import { atomWithStorage } from 'jotai/utils';
// Automatically syncs with localStorage
const themeAtom = atomWithStorage('theme', 'light');
const userPrefsAtom = atomWithStorage('userPrefs', {
notifications: true,
language: 'en',
});
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}
Jotai vs Zustand
| Aspect | Jotai | Zustand |
|---|---|---|
| Mental model | Atoms (bottom-up) | Store (top-down) |
| State structure | Distributed atoms | Single object |
| Re-renders | Per-atom subscriptions | Selector-based |
| Derived state | Built-in atoms | get() in store |
💡 Best Practices
- • Keep atoms small and focused
- • Use derived atoms for computed values
- • Use useAtomValue/useSetAtom for read-only/write-only access
- • Organize atoms by feature or domain
- • Use atomWithStorage for persistent state