Files
v10/.claude/skills/api/references/state.md
T

379 lines
8.9 KiB
Markdown

# State Management Patterns
Patterns for designing and using state management in TypeScript libraries.
## Contents
- [Mental Models](#mental-models) — Zustand, Jotai, XState, Valtio
- [Core Subscription Interface](#core-subscription-interface)
- [Middleware as Higher-Order Functions](#middleware-as-higher-order-functions)
- [Slice Pattern](#slice-pattern)
- [Atomic Composition](#atomic-composition-jotai-pattern)
- [Selector Pattern](#selector-pattern)
- [Split Stores When Truly Isolated](#split-stores-when-truly-isolated)
- [Cross-Store Access](#cross-store-access)
- [Derived State Patterns](#derived-state-patterns)
- [Optimistic Updates](#optimistic-updates)
- [State Initialization](#state-initialization)
- [Request State Pattern](#request-state-pattern)
- [Proxy-Based Reactivity](#proxy-based-reactivity-valtio)
- [Presets as Transparent Collections](#presets-as-transparent-collections)
- [Dev Tools Integration](#dev-tools-integration)
## Mental Models
Daishi Kato created Zustand, Jotai, and Valtio with intentionally different architectures because **different problems need different mental models**.
| Model | Mental Model | Best For |
| ---------------------- | ------------------------------------- | ------------------------------ |
| Top-down (Zustand) | Single store, slice into pieces | Module state, non-React access |
| Bottom-up (Jotai) | Composable atoms, build up | useState replacement |
| State machine (XState) | Explicit states and transitions | Complex workflows |
| Proxy (Valtio) | Mutable-looking, immutable underneath | Mutable-preferring devs |
**The principle:** Don't force a mental model. Choose based on how developers naturally think about the domain.
---
## Core Subscription Interface
The minimal interface any framework can consume:
```ts
interface Store<T> {
get(): T;
subscribe(callback: (value: T) => void): () => void;
}
```
This enables framework adapters to be ~10 lines using `useSyncExternalStore` (React), `shallowRef` (Vue), or `createSignal` (Solid).
---
## Middleware as Higher-Order Functions
Middleware wraps the state creator, not the store:
```ts
// Zustand pattern
create(
devtools(
persist(
immer((set) => ({ count: 0 })),
{ name: 'store' }
)
)
);
```
**Benefits:**
- Composable in any order
- Type-safe (each middleware can modify types)
- Tree-shakeable (unused middleware not bundled)
---
## Slice Pattern
Split state by concern, combine at creation time:
```ts
// Each module factory receives (set, get) and returns state slice
const createVolumeSlice = (set, get) => ({
volume: 1,
setVolume: (v) => set({ volume: v }),
});
const createPlaybackSlice = (set, get) => ({
playing: false,
play: () => set({ playing: true }),
});
// Combine by spreading at creation time
const useStore = create((...a) => ({
...createVolumeSlice(...a),
...createPlaybackSlice(...a),
}));
```
**Why creation-time composition:**
| Benefit | Why |
| --------------------------- | -------------------------------- |
| Cross-module access natural | `get`/`set` see entire store |
| Atomic updates span modules | One `set` updates multiple areas |
| Middleware applies to whole | No per-module confusion |
| Tree-shaking possible | Unused modules excluded |
---
## Atomic Composition (Jotai Pattern)
Build complex state from simple atoms:
```ts
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
const asyncAtom = atom(async (get) => fetch(`/api/${get(countAtom)}`));
```
**Benefits:**
- Fine-grained subscriptions
- Derived state is automatic
- Async handled uniformly
---
## Selector Pattern
Minimize re-renders with selectors:
```ts
// Subscribes to entire state (causes re-renders)
const state = useStore();
// Subscribes to selected slice (fine-grained)
const volume = useStore((state) => state.volume);
```
**Implementation considerations:**
- Shallow equality by default
- Custom equality function option
- Memoized selector support
---
## Split Stores When Truly Isolated
| Situation | Recommendation | Why |
| ----------------------------- | ------------------------- | -------------------------- |
| Totally isolated concerns | Multiple stores | Cleaner boundaries |
| Might ever want cross-updates | Single store with modules | Atomic updates possible |
| Multiple contexts needed | Single store | Simpler provider hierarchy |
**Heuristic:** "When you feel that something is getting difficult to maintain, that's the moment to start splitting."
---
## Cross-Store Access
When stores must communicate, make the relationship explicit:
```ts
interface AppContext {
userStore: Store<UserState>;
settingsStore: Store<SettingsState>;
}
function createFeatureStore(context: AppContext) {
return create((set, get) => ({
syncWithUser: () => {
const user = context.userStore.getState();
set({ userId: user.id });
},
}));
}
```
**Why explicit references:**
- Coordination goes through typed interface
- TypeScript catches mismatches
- No hidden global state
- Easy to test with mock stores
---
## Derived State Patterns
### Selector-Based
Compute derived state at subscription time:
```ts
store.subscribe(
(state) => state.items.reduce((sum, item) => sum + item.price, 0),
(totalPrice) => updateUI(totalPrice)
);
```
### Computed Atoms
Define derived state as a dependency graph:
```ts
const itemsAtom = atom([]);
const totalPriceAtom = atom((get) => get(itemsAtom).reduce((sum, item) => sum + item.price, 0));
```
| Pattern | Use When |
| ------------------ | ----------------------------------- |
| Selectors | Derived data varies by consumer |
| Computed atoms | Derived data shared across app |
| Memoized selectors | Expensive computation needs caching |
---
## Optimistic Updates
For responsive UI during async operations:
```ts
async function updateVolume(newVolume: number) {
const previous = store.getState().volume;
// Optimistic update
store.setState({ volume: newVolume });
try {
await api.setVolume(newVolume);
} catch {
// Rollback on failure
store.setState({ volume: previous });
}
}
```
---
## State Initialization
### Lazy Initialization
Defer expensive work until needed:
```ts
createStore((set) => ({
data: null,
initialize: async () => {
const data = await fetchExpensiveData();
set({ data });
},
}));
```
### Hydration Support
Allow external state injection (SSR, persistence):
```ts
createStore((set) => ({
data: null,
hydrate: (serverState) => set(serverState),
}));
// Client: store.getState().hydrate(window.__INITIAL_STATE__)
```
### Reset Pattern
Return to known initial state:
```ts
const initialState = { count: 0, items: [] };
createStore((set) => ({
...initialState,
reset: () => set(initialState),
}));
```
---
## Request State Pattern
Standardized shape for async operations:
```ts
type RequestState<T, E = Error> =
| { status: 'idle' }
| { status: 'pending' }
| { status: 'success'; data: T; timestamp: number }
| { status: 'error'; error: E; timestamp: number };
```
Helper functions:
```ts
function isLoading(state: RequestState<unknown>): boolean;
function isSuccess<T>(state: RequestState<T>): state is SuccessState<T>;
function isError<E>(state: RequestState<unknown, E>): state is ErrorState<E>;
```
---
## Proxy-Based Reactivity (Valtio)
Mutate naturally, get automatic tracking:
```ts
const state = proxy({ count: 0, nested: { value: 1 } });
// Mutations work directly
state.count++;
state.nested.value = 2;
// Subscribe to changes
subscribe(state, () => console.log('changed'));
// React hook tracks accessed properties
const snap = useSnapshot(state);
```
**Trade-offs:**
- Simpler mental model
- Proxies can obscure types
- Deep reactivity automatic but sometimes unwanted
---
## Presets as Transparent Collections
Make presets visible and extensible:
```ts
// Good: Preset is just an array, visible and extensible
const websitePreset = [analyticsModule, cachingModule, loggingModule];
createStore({ modules: websitePreset });
createStore({ modules: [...websitePreset, customModule] });
// Bad: Preset hides internals
createStore({ preset: 'website' }); // What's in it?
```
---
## Dev Tools Integration
Patterns for debugging support:
```ts
// Named stores for devtools
const useStore = create(
devtools(
(set) => ({ ... }),
{ name: 'MediaStore' }
)
)
// Action names
set({ volume: 0.5 }, false, 'setVolume')
// Time-travel support via snapshots
const snapshot = store.getState()
store.setState(previousSnapshot)
```
---
## See Also
- [Principles](principles.md) — composition and extensibility
- [Extensibility](extensibility.md) — middleware patterns
- [Libraries](libraries.md) — reference implementations