Claude-skill-registry jotai-patterns
Jotai atomic state management patterns. Use when implementing fine-grained reactive state.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/jotai-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-jotai-patterns && rm -rf "$T"
manifest:
skills/data/jotai-patterns/SKILL.mdsource content
Jotai Patterns Skill
This skill covers Jotai atomic state management for React applications.
When to Use
Use this skill when:
- Need fine-grained reactivity
- Building complex state dependencies
- Want provider-less global state
- Prefer atomic/bottom-up state design
Core Principle
ATOMS ARE PRIMITIVES - Build complex state from simple atoms. Components subscribe only to atoms they use.
Basic Atoms
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; // Primitive atom const countAtom = atom(0); // Usage function Counter(): React.ReactElement { const [count, setCount] = useAtom(countAtom); return ( <button onClick={() => setCount((c) => c + 1)}> Count: {count} </button> ); } // Read-only usage function CountDisplay(): React.ReactElement { const count = useAtomValue(countAtom); return <span>Count: {count}</span>; } // Write-only usage function IncrementButton(): React.ReactElement { const setCount = useSetAtom(countAtom); return <button onClick={() => setCount((c) => c + 1)}>+</button>; }
Derived Atoms
Read-Only Derived
const countAtom = atom(0); // Derived atom (read-only) const doubleCountAtom = atom((get) => get(countAtom) * 2); const isEvenAtom = atom((get) => get(countAtom) % 2 === 0); // Multiple dependencies const usersAtom = atom<User[]>([]); const filterAtom = atom(''); const filteredUsersAtom = atom((get) => { const users = get(usersAtom); const filter = get(filterAtom).toLowerCase(); if (!filter) return users; return users.filter((user) => user.name.toLowerCase().includes(filter) ); });
Read-Write Derived
const celsiusAtom = atom(0); // Read-write derived atom const fahrenheitAtom = atom( (get) => get(celsiusAtom) * (9 / 5) + 32, (get, set, newFahrenheit: number) => { set(celsiusAtom, (newFahrenheit - 32) * (5 / 9)); } ); // Usage - both read and write work function TemperatureConverter(): React.ReactElement { const [celsius, setCelsius] = useAtom(celsiusAtom); const [fahrenheit, setFahrenheit] = useAtom(fahrenheitAtom); return ( <div> <input type="number" value={celsius} onChange={(e) => setCelsius(Number(e.target.value))} /> °C = <input type="number" value={fahrenheit} onChange={(e) => setFahrenheit(Number(e.target.value))} /> °F </div> ); }
Async Atoms
// Async read atom const userAtom = atom(async () => { const response = await fetch('/api/user'); return response.json() as Promise<User>; }); // Usage with Suspense function UserProfile(): React.ReactElement { const user = useAtomValue(userAtom); return <h1>{user.name}</h1>; } function App(): React.ReactElement { return ( <Suspense fallback={<Loading />}> <UserProfile /> </Suspense> ); } // Async with dependencies const userIdAtom = atom('1'); const userDataAtom = atom(async (get) => { const userId = get(userIdAtom); const response = await fetch(`/api/users/${userId}`); return response.json() as Promise<User>; });
Write-Only Atoms (Actions)
const todosAtom = atom<Todo[]>([]); // Write-only atom for actions const addTodoAtom = atom(null, (get, set, text: string) => { const newTodo: Todo = { id: crypto.randomUUID(), text, completed: false, }; set(todosAtom, [...get(todosAtom), newTodo]); }); const toggleTodoAtom = atom(null, (get, set, id: string) => { set( todosAtom, get(todosAtom).map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }); const removeTodoAtom = atom(null, (get, set, id: string) => { set( todosAtom, get(todosAtom).filter((todo) => todo.id !== id) ); }); // Usage function AddTodo(): React.ReactElement { const addTodo = useSetAtom(addTodoAtom); const [text, setText] = useState(''); const handleSubmit = (e: FormEvent): void => { 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> ); }
Atom Families
import { atomFamily } from 'jotai/utils'; // Create atoms dynamically const todoAtomFamily = atomFamily((id: string) => atom<Todo | null>(null) ); // Usage function TodoItem({ id }: { id: string }): React.ReactElement { const [todo, setTodo] = useAtom(todoAtomFamily(id)); if (!todo) return null; return ( <div> <input type="checkbox" checked={todo.completed} onChange={() => setTodo({ ...todo, completed: !todo.completed }) } /> {todo.text} </div> ); }
Persistence
import { atomWithStorage } from 'jotai/utils'; // Persisted to localStorage const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light'); const settingsAtom = atomWithStorage('settings', { notifications: true, language: 'en', }); // Usage - automatically syncs with localStorage function ThemeToggle(): React.ReactElement { const [theme, setTheme] = useAtom(themeAtom); return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> {theme} </button> ); }
Reset Atoms
import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'; const formAtom = atomWithReset({ name: '', email: '', message: '', }); function ContactForm(): React.ReactElement { const [form, setForm] = useAtom(formAtom); const resetForm = useResetAtom(formAtom); // Or use RESET symbol // setForm(RESET); return ( <form> <input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /> <button type="button" onClick={resetForm}> Reset </button> </form> ); }
Combining with TanStack Query
import { atomWithQuery } from 'jotai-tanstack-query'; const userIdAtom = atom('1'); const userAtom = atomWithQuery((get) => ({ queryKey: ['user', get(userIdAtom)], queryFn: () => fetchUser(get(userIdAtom)), })); function UserProfile(): React.ReactElement { const [{ data: user, isLoading }] = useAtom(userAtom); if (isLoading) return <Loading />; return <div>{user?.name}</div>; }
DevTools
import { useAtomsDebugValue } from 'jotai-devtools'; function App(): React.ReactElement { useAtomsDebugValue(); // Shows atoms in React DevTools return <Main />; }
Best Practices
- Start with primitive atoms - Build complex from simple
- Use derived atoms - Avoid duplicating state
- Keep atoms small - One concern per atom
- Use atom families - For dynamic/collection state
- Prefer useAtomValue/useSetAtom - When only reading or writing
Jotai vs Zustand
| Feature | Jotai | Zustand |
|---|---|---|
| Model | Bottom-up (atoms) | Top-down (store) |
| Subscriptions | Automatic (fine-grained) | Manual (selectors) |
| Provider | Optional | Not needed |
| Async | Built-in | Manual |
| DevTools | Separate package | Middleware |
| Best for | Complex dependencies | Simple global state |
When to Use Jotai
- Complex state dependencies
- Fine-grained re-render control
- Derived state calculations
- Code splitting state
- When you think in "atoms"
Notes
- Atoms are not stored in a single object
- Each atom can be code-split
- No provider needed (uses React context internally)
- Works great with React Suspense