Claude-skill-registry jotai
Manages React state with Jotai including atoms, derived atoms, async atoms, and utilities. Use when building React applications needing atomic state, bottom-up state management, or fine-grained updates.
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" ~/.claude/skills/majiayu000-claude-skill-registry-jotai && rm -rf "$T"
manifest:
skills/data/jotai/SKILL.mdsource content
Jotai
Primitive and flexible state management for React.
Quick Start
Install:
npm install jotai
Basic usage:
import { atom, useAtom } from 'jotai'; const countAtom = atom(0); function Counter() { const [count, setCount] = useAtom(countAtom); return ( <button onClick={() => setCount(c => c + 1)}> Count: {count} </button> ); }
Atoms
Primitive Atoms
import { atom } from 'jotai'; // Number const countAtom = atom(0); // String const nameAtom = atom(''); // Boolean const isOpenAtom = atom(false); // Object const userAtom = atom({ name: '', email: '' }); // Array const todosAtom = atom<Todo[]>([]);
Using Atoms
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; function Component() { // Read and write const [count, setCount] = useAtom(countAtom); // Read only const count = useAtomValue(countAtom); // Write only (no re-render on change) const setCount = useSetAtom(countAtom); return ( <div> <p>{count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> </div> ); }
Derived Atoms
Read-Only Derived
import { atom } from 'jotai'; const countAtom = atom(0); // Simple derived const doubledAtom = atom((get) => get(countAtom) * 2); // Derived from multiple atoms const firstNameAtom = atom('John'); const lastNameAtom = atom('Doe'); const fullNameAtom = atom((get) => { return `${get(firstNameAtom)} ${get(lastNameAtom)}`; }); // Filtered list const todosAtom = atom<Todo[]>([]); const completedTodosAtom = atom((get) => { return get(todosAtom).filter(todo => todo.completed); });
Read-Write Derived
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
// Action atom (no read value) const incrementAtom = atom(null, (get, set) => { set(countAtom, get(countAtom) + 1); }); // Usage const increment = useSetAtom(incrementAtom); <button onClick={increment}>+1</button> // 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
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 <div>{user.name}</div>; } function App() { return ( <Suspense fallback={<div>Loading...</div>}> <UserProfile /> </Suspense> ); }
Async Atom with Error Handling
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 <div>Loading...</div>; } if (userLoadable.state === 'hasError') { return <div>Error: {userLoadable.error.message}</div>; } return <div>{userLoadable.data.name}</div>; }
Refreshable Async Atom
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 ( <div> <pre>{JSON.stringify(data)}</pre> <button onClick={refresh}>Refresh</button> </div> ); }
Jotai Utilities
atomWithStorage
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 ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Theme: {theme} </button> ); }
atomWithReset
import { atomWithReset, useResetAtom } from 'jotai/utils'; const countAtom = atomWithReset(0); function Counter() { const [count, setCount] = useAtom(countAtom); const reset = useResetAtom(countAtom); return ( <div> <p>{count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> <button onClick={reset}>Reset</button> </div> ); }
atomFamily
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 ( <div> <input value={todo.text} onChange={(e) => setTodo({ ...todo, text: e.target.value })} /> </div> ); }
selectAtom
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
import { splitAtom } from 'jotai/utils'; const todosAtom = atom<Todo[]>([]); const todoAtomsAtom = splitAtom(todosAtom); function TodoList() { const [todoAtoms, dispatch] = useAtom(todoAtomsAtom); return ( <ul> {todoAtoms.map((todoAtom) => ( <TodoItem key={`${todoAtom}`} todoAtom={todoAtom} onRemove={() => dispatch({ type: 'remove', atom: todoAtom })} /> ))} </ul> ); } function TodoItem({ todoAtom, onRemove }) { const [todo, setTodo] = useAtom(todoAtom); return ( <li> <input type="checkbox" checked={todo.completed} onChange={(e) => setTodo({ ...todo, completed: e.target.checked })} /> {todo.text} <button onClick={onRemove}>Delete</button> </li> ); }
focusAtom
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 <input value={city} onChange={(e) => setCity(e.target.value)} />; }
Provider
Scoped State
import { Provider, createStore } from 'jotai'; const myStore = createStore(); function App() { return ( <Provider store={myStore}> <Counter /> </Provider> ); }
Multiple Providers
function App() { return ( <Provider> <Counter /> {/* Uses Provider 1 */} <Provider> <Counter /> {/* Uses Provider 2 - isolated state */} </Provider> </Provider> ); }
Store API
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
import { useAtomsDebugValue } from 'jotai-devtools'; function DebugAtoms() { useAtomsDebugValue(); return null; } function App() { return ( <> <DebugAtoms /> <Counter /> </> ); }
Patterns
Form State
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
const todosAtom = atom<Todo[]>([]); 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
- Define atoms outside components - Avoid recreating atoms
- Use derived atoms - Compose complex state from primitives
- Use utilities - atomWithStorage, atomFamily, splitAtom
- Keep atoms small - One piece of state per atom
- 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 - Jotai utilities
- references/async.md - Async patterns
- references/integration.md - Framework integration