Skip to main content

ADR-003: Zustand for State Management

Status

Accepted

Context

The config-driven UI requires a state management solution that:

  1. Supports dynamic schema: Store shape defined by tenant config
  2. Provides dot-notation access: store.user.profile.email
  3. Enables selective persistence: Per-slice sessionStorage
  4. Integrates with DevTools: Debugging support
  5. 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:

  1. Dynamic store creation: createGlobalStore(config) builds store from config
  2. Immer middleware: Immutable updates with mutable syntax
  3. Persist middleware: Selective sessionStorage persistence
  4. 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:

HookPurpose
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