src/skills/web-state-jotai/SKILL.md
Atomic state management with auto-dependency tracking
npx skillsauth add agents-inc/skills web-state-jotaiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Jotai provides atomic, bottom-up state management with automatic dependency tracking. Define atoms at module level (never inside components). Use primitive atoms for values, derived atoms for computed state, write-only atoms for actions, and async atoms with Suspense for loading. Components only re-render when their specific atoms change. The
atomFamilyutility is deprecated -- use thejotai-familypackage for new code.
<critical_requirements>
(You MUST define atoms OUTSIDE components -- creating atoms inside render causes broken state)
(You MUST wrap async atom consumers in Suspense boundaries -- async atoms trigger Suspense by default)
(You MUST use write atoms (action atoms) to encapsulate multi-atom updates and post-async state changes)
(You MUST use jotai-family package instead of deprecated atomFamily from jotai/utils for new code)
</critical_requirements>
Auto-detection: Jotai, jotai, atom, useAtom, useAtomValue, useSetAtom, atomWithStorage, atomFamily, splitAtom, selectAtom, derived atom, loadable, unwrap, createStore, Provider store
When to use:
Key patterns covered:
When NOT to use:
Jotai takes an atomic, bottom-up approach to state management. The core principle: "Anything that can be derived from the application state should be derived automatically."
Consider atoms like cells in a spreadsheet:
Key characteristics:
Mental Model: Instead of one large store, you have many small atoms that can be combined. This creates natural code splitting and enables precise re-render optimization without manual memoization.
</philosophy>The simplest atom type -- holds a single value with automatic type inference. Always define at module level.
import { atom } from "jotai";
const INITIAL_COUNT = 0;
const countAtom = atom(INITIAL_COUNT);
const userAtom = atom<User | null>(null);
export { countAtom, userAtom };
Why good: Module-level definition persists state, type inference works automatically, explicit typing only for unions/nullable
See examples/core.md Pattern 1 for complete examples with type patterns.
Compute values from other atoms -- dependencies tracked automatically, cached until dependencies change.
const subtotalAtom = atom((get) => get(priceAtom) * get(quantityAtom));
const taxAtom = atom((get) => get(subtotalAtom) * get(taxRateAtom));
const totalAtom = atom((get) => get(subtotalAtom) + get(taxAtom));
Why good: Automatic dependency tracking, cached computation, chain of derivations is composable
See examples/core.md Pattern 2 for derived atom chains and conditional derivations.
Encapsulate side effects and multi-atom updates. First argument is null (no read value).
const resetAllAtom = atom(null, (get, set) => {
set(countAtom, 0);
set(itemsAtom, []);
set(selectedAtom, null);
});
Why good: Actions are reusable, enables code splitting, multiple atoms updated atomically
See examples/core.md Pattern 3 for action atoms with arguments.
Atoms that can both read derived state and accept writes. Useful for lens-like property access on larger objects.
const nameAtom = atom(
(get) => get(userAtom).name,
(get, set, newName: string) => {
set(userAtom, { ...get(userAtom), name: newName });
},
);
Why good: Granular read/write access to object properties, keeps parent intact
See examples/core.md Pattern 4 for lens patterns and transformations.
Async atoms trigger Suspense by default. Use loadable() for manual loading states or unwrap() for fallback values.
// Triggers Suspense -- wrap consumer in <Suspense>
const userAtom = atom(async (get) => {
const id = get(userIdAtom);
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<User>;
});
// Non-Suspense alternative
const loadableUserAtom = loadable(userAtom);
// Returns { state: 'loading' } | { state: 'hasData', data } | { state: 'hasError', error }
Why good: First-class Suspense integration, loadable provides type-safe discriminated union
See examples/async.md for Suspense setup, loadable/unwrap patterns, and AbortController support.
Persist atom values to localStorage, sessionStorage, or custom storage. Use RESET symbol to restore defaults.
import { atomWithStorage, RESET } from "jotai/utils";
const STORAGE_KEY = "app-theme";
const themeAtom = atomWithStorage<Theme>(STORAGE_KEY, "light");
// setTheme(RESET) restores to "light"
Gotcha: Default behavior renders initial value first, then stored value (flicker). Set { getOnInit: true } for immediate stored value, but beware SSR hydration issues.
See examples/persistence.md Pattern 1 for storage variants and reset patterns.
Split an array atom into individual item atoms. Each item gets its own atom -- updating one item only re-renders that item's component.
import { splitAtom } from "jotai/utils";
const todosAtom = atom<Todo[]>([]);
const todoAtomsAtom = splitAtom(todosAtom, (todo) => todo.id);
// keyExtractor prevents unnecessary atom recreation on reorder
Why good: Per-item re-render isolation, dispatch for add/remove/move operations
See examples/persistence.md Pattern 3 for list rendering with dispatch.
Custom stores enable state access outside React, test isolation, and multi-store architectures.
import { createStore, Provider } from "jotai";
const store = createStore();
store.set(countAtom, 10); // Access outside React
store.sub(countAtom, () => { /* react to changes */ });
// Provider with store for isolation
<Provider store={store}><App /></Provider>
Gotcha: Each Provider creates isolated state. Atoms in different Providers do not share values unless given the same store instance.
See examples/testing.md for store, provider, and test isolation patterns.
</patterns>Detailed Resources:
<red_flags>
High Priority Issues:
atomFamily from jotai/utils -- will be removed in v3, use jotai-family packageMedium Priority Issues:
selectAtom as a primary pattern -- official docs call it an "escape hatch", prefer derived atomskeyExtractor with splitAtom for items with IDs -- causes unnecessary atom recreationatomFamily without memory cleanup -- remove() or setShouldRemove() required to prevent leaksGotchas & Edge Cases:
loadable() returns discriminated union -- always check state property firstRESET symbol is special -- pass to setter to reset atomWithStorage to initial valueatomWithStorage default renders initial value first, then stored value (flicker)getOnInit: true avoids flicker but may cause SSR hydration mismatches</red_flags>
<critical_reminders>
(You MUST define atoms OUTSIDE components -- creating atoms inside render causes broken state)
(You MUST wrap async atom consumers in Suspense boundaries -- async atoms trigger Suspense by default)
(You MUST use write atoms (action atoms) to encapsulate multi-atom updates and post-async state changes)
(You MUST use jotai-family package instead of deprecated atomFamily from jotai/utils for new code)
Failure to follow these rules will cause state corruption, Suspense errors, and deprecated API usage.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety