([]);
const completedTodosAtom = atom((get) => {
return get(todosAtom).filter(todo => todo.completed);
});
```
### Read-Write Derived
```tsx
const countAtom = atom(0);
// Derived with custom setter
const doubledAtom = atom(
(get) => get(countAtom) * 2,
(get, set, newValue: number) => {
set(countAtom, newValue / 2);
}
);
// Toggle atom
const isOpenAtom = atom(false);
const toggleAtom = atom(
(get) => get(isOpenAtom),
(get, set) => {
set(isOpenAtom, !get(isOpenAtom));
}
);
```
### Write-Only Atoms
```tsx
// Action atom (no read value)
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1);
});
// Usage
const increment = useSetAtom(incrementAtom);
// Action with parameters
const addTodoAtom = atom(null, (get, set, text: string) => {
const todos = get(todosAtom);
set(todosAtom, [...todos, { id: Date.now(), text, completed: false }]);
});
```
## Async Atoms
### Basic Async Atom
```tsx
import { atom, useAtomValue } from 'jotai';
import { Suspense } from 'react';
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
const id = get(userIdAtom);
const response = await fetch(`/api/users/${id}`);
return response.json();
});
function UserProfile() {
const user = useAtomValue(userAtom);
return {user.name}
;
}
function App() {
return (
Loading...}>
);
}
```
### Async Atom with Error Handling
```tsx
import { atom, useAtom } from 'jotai';
import { loadable } from 'jotai/utils';
const userAtom = atom(async (get) => {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
});
const loadableUserAtom = loadable(userAtom);
function UserProfile() {
const [userLoadable] = useAtom(loadableUserAtom);
if (userLoadable.state === 'loading') {
return Loading...
;
}
if (userLoadable.state === 'hasError') {
return Error: {userLoadable.error.message}
;
}
return {userLoadable.data.name}
;
}
```
### Refreshable Async Atom
```tsx
import { atom, useAtom, useSetAtom } from 'jotai';
const fetchCountAtom = atom(0);
const dataAtom = atom(async (get) => {
get(fetchCountAtom); // Dependency for refresh
const response = await fetch('/api/data');
return response.json();
});
const refreshAtom = atom(null, (get, set) => {
set(fetchCountAtom, (c) => c + 1);
});
function DataComponent() {
const data = useAtomValue(dataAtom);
const refresh = useSetAtom(refreshAtom);
return (
{JSON.stringify(data)}
);
}
```
## Jotai Utilities
### atomWithStorage
```tsx
import { atomWithStorage } from 'jotai/utils';
// Persists to localStorage
const themeAtom = atomWithStorage('theme', 'light');
// With sessionStorage
const sessionAtom = atomWithStorage('session', null, sessionStorage);
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
);
}
```
### atomWithReset
```tsx
import { atomWithReset, useResetAtom } from 'jotai/utils';
const countAtom = atomWithReset(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const reset = useResetAtom(countAtom);
return (
{count}
);
}
```
### atomFamily
```tsx
import { atomFamily } from 'jotai/utils';
// Create a family of atoms with parameter
const todoAtomFamily = atomFamily((id: string) =>
atom({ id, text: '', completed: false })
);
function TodoItem({ id }: { id: string }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id));
return (
setTodo({ ...todo, text: e.target.value })}
/>
);
}
```
### selectAtom
```tsx
import { selectAtom } from 'jotai/utils';
const userAtom = atom({ name: 'John', age: 30, email: 'john@example.com' });
// Only re-renders when name changes
const nameAtom = selectAtom(userAtom, (user) => user.name);
// With equality function
const ageAtom = selectAtom(
userAtom,
(user) => user.age,
(a, b) => a === b
);
```
### splitAtom
```tsx
import { splitAtom } from 'jotai/utils';
const todosAtom = atom([]);
const todoAtomsAtom = splitAtom(todosAtom);
function TodoList() {
const [todoAtoms, dispatch] = useAtom(todoAtomsAtom);
return (
{todoAtoms.map((todoAtom) => (
dispatch({ type: 'remove', atom: todoAtom })}
/>
))}
);
}
function TodoItem({ todoAtom, onRemove }) {
const [todo, setTodo] = useAtom(todoAtom);
return (
setTodo({ ...todo, completed: e.target.checked })}
/>
{todo.text}
);
}
```
### focusAtom
```tsx
import { focusAtom } from 'jotai-optics';
const userAtom = atom({ name: 'John', address: { city: 'NYC', zip: '10001' } });
// Focus on nested property
const cityAtom = focusAtom(userAtom, (optic) => optic.prop('address').prop('city'));
function CityInput() {
const [city, setCity] = useAtom(cityAtom);
return setCity(e.target.value)} />;
}
```
## Provider
### Scoped State
```tsx
import { Provider, createStore } from 'jotai';
const myStore = createStore();
function App() {
return (
);
}
```
### Multiple Providers
```tsx
function App() {
return (
{/* Uses Provider 1 */}
{/* Uses Provider 2 - isolated state */}
);
}
```
### Store API
```tsx
import { createStore } from 'jotai';
const store = createStore();
// Get value outside React
const count = store.get(countAtom);
// Set value outside React
store.set(countAtom, 10);
// Subscribe to changes
const unsub = store.sub(countAtom, () => {
console.log('Count changed:', store.get(countAtom));
});
```
## DevTools
```tsx
import { useAtomsDebugValue } from 'jotai-devtools';
function DebugAtoms() {
useAtomsDebugValue();
return null;
}
function App() {
return (
<>
>
);
}
```
## Patterns
### Form State
```tsx
const formAtom = atom({
name: '',
email: '',
message: '',
});
const nameAtom = focusAtom(formAtom, (o) => o.prop('name'));
const emailAtom = focusAtom(formAtom, (o) => o.prop('email'));
const messageAtom = focusAtom(formAtom, (o) => o.prop('message'));
const isValidAtom = atom((get) => {
const form = get(formAtom);
return form.name.length > 0 && form.email.includes('@');
});
```
### Optimistic Updates
```tsx
const todosAtom = atom([]);
const addTodoAtom = atom(null, async (get, set, text: string) => {
const tempId = Date.now();
const newTodo = { id: tempId, text, completed: false, pending: true };
// Optimistic update
set(todosAtom, [...get(todosAtom), newTodo]);
try {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
});
const savedTodo = await response.json();
// Replace temp with saved
set(todosAtom, get(todosAtom).map(t =>
t.id === tempId ? { ...savedTodo, pending: false } : t
));
} catch (error) {
// Rollback
set(todosAtom, get(todosAtom).filter(t => t.id !== tempId));
}
});
```
## Best Practices
1. **Define atoms outside components** - Avoid recreating atoms
2. **Use derived atoms** - Compose complex state from primitives
3. **Use utilities** - atomWithStorage, atomFamily, splitAtom
4. **Keep atoms small** - One piece of state per atom
5. **Use Suspense for async** - Or loadable for manual handling
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Creating atoms in components | Define atoms outside components |
| Not wrapping async in Suspense | Add Suspense boundary |
| Mutating objects directly | Return new objects |
| Overusing derived atoms | Keep derivation tree shallow |
| Missing equality functions | Use selectAtom with comparator |
## Reference Files
- [references/utilities.md](references/utilities.md) - Jotai utilities
- [references/async.md](references/async.md) - Async patterns
- [references/integration.md](references/integration.md) - Framework integration