ADR-003: Zustand for State Management
Status
Accepted
Context
The config-driven UI requires a state management solution that:
- Supports dynamic schema: Store shape defined by tenant config
- Provides dot-notation access:
store.user.profile.email - Enables selective persistence: Per-slice sessionStorage
- Integrates with DevTools: Debugging support
- Works with React 18+: Concurrent features compatible
Options considered:
- Redux: Powerful but verbose, overkill for this use case
- Jotai/Recoil: Atomic state, harder to persist complex slices
- Zustand: Simple, flexible, supports middleware composition
Decision
We will use Zustand with these patterns:
- Dynamic store creation:
createGlobalStore(config)builds store from config - Immer middleware: Immutable updates with mutable syntax
- Persist middleware: Selective sessionStorage persistence
- DevTools middleware: Named actions for debugging
// packages/client/src/config/createGlobalStore.ts
export function createGlobalStore(config: EAIConfig) {
return create(
devtools(
persist(
immer((set, get) => ({
...buildInitialState(config.store),
set: (path, value, caller) => {
set((state) => setByPath(state, path, value),
false,
`set/${path} (${caller})`);
},
get: (path) => getByPath(get(), path),
})),
{
name: 'eai-store',
partialize: filterPersistedSlices(config.store),
}
)
)
);
}
Store Hooks
Custom hooks provide ergonomic access:
| Hook | Purpose |
|---|---|
useStoreValue(path) | Reactive read at path |
useSetStore() | Get setter function |
useGlobalSelector(fn) | Custom selector |
useStoreGetter(path) | Non-reactive getter |
useResetSlice(name) | Reset slice to initial |
Consequences
Positive
- Minimal boilerplate: No action creators or reducers
- Type inference: Full TypeScript support
- Flexible middleware: Compose persistence, DevTools, etc.
- Small bundle: ~2KB gzipped
Negative
- Learning curve: Different patterns than Redux
- Middleware order: Must be composed correctly
- No time-travel: DevTools integration is basic
Neutral
- Single store: All state in one place (by design)
- Selector patterns: Similar to Redux useSelector
Related ADRs
- ADR-001: Config-Driven - Store defined by config