State Managers

Zustand

Zustand is Flux's native state manager — no adapter, no wrapper, no glue code. Pass the store hook directly to register() and Flux calls store.setState() on it directly.

Prerequisites
  • A Zustand store created with create() — see Zustand's docs if you're new to it
  • Familiarity with register() — see api/register
1

A plain Zustand store with dual-payload support

Writing a store for Flux requires structuring setState to handle both initial array payloads (from cold-boot IndexedDB hydration and bootstrap) and single live delta frames (CREATE, sparse field UPDATE, and DELETE):
typescript
1// src/store/useTasksStore.ts
2import { create } from 'zustand'
3
4export interface Task {
5 id: string
6 title: string
7 description: string
8 status: string
9}
10
11interface TasksStore {
12 tasks: Task[]
13 isHydrated: boolean
14 // Required by Flux — dispatchToStore calls store.setState(data) directly.
15 // Must handle both array hydration payloads (bootstrap/IDB cold boot)
16 // and single delta frame objects (CREATE, sparse UPDATE, DELETE).
17 setState: (data: any) => void
18}
19
20export const useTasksStore = create<TasksStore>((set, get) => ({
21 tasks: [],
22 isHydrated: false,
23 setState: (data: any) => {
24 if (!data) return
25
26 // ── 1. Array payload — bootstrap, IDB hydration, or full sync ──
27 if (Array.isArray(data)) {
28 set({ tasks: [...data], isHydrated: true })
29 return
30 }
31
32 // ── 2. Object payload — single delta frame from live stream ────
33 if (typeof data === 'object') {
34 const current = get().tasks
35
36 // DELETE
37 if (data.op === 'DELETE' || data.eventType === 'DELETE') {
38 const id = String(data.id ?? data._id ?? data.data?.id ?? '')
39 if (id) set({ tasks: current.filter((t) => t.id !== id) })
40 return
41 }
42
43 const rawRecord = data.data ?? data.new ?? data
44 const recordId = String(rawRecord?.id ?? rawRecord?._id ?? data.id ?? '')
45
46 if (!recordId) return
47
48 const idx = current.findIndex((t) => t.id === recordId)
49 if (idx !== -1) {
50 // Sparse UPDATE — shallow merge fields onto existing record
51 const next = [...current]
52 next[idx] = { ...next[idx], ...rawRecord, id: recordId }
53 set({ tasks: next, isHydrated: true })
54 } else {
55 // CREATE — prepend new record
56 set({
57 tasks: [{ id: recordId, ...rawRecord }, ...current],
58 isHydrated: true,
59 })
60 }
61 }
62 },
63}))
2

Registering it

The store field takes the Zustand hook directly — no createXAdapter() call in between, unlike Redux, Jotai, or Stunk.
typescript
1import { useTasksStore } from './useTasksStore'
2
3flux.register({
4 store: useTasksStore,
5 channel: 'tasks',
6 event: 'UPDATE',
7 idbKey: 'tasks:latest',
8 ttl: 'medium',
9 ingestionType: 'COLLECTION_ALL',
10 diffBeforeUpdate: true,
11})
12
13// No adapter import, no wrapper call — the store field takes the
14// Zustand hook directly. This is the only state manager Flux treats
15// as a first-class citizen with zero glue code.
3

Why Zustand needs no adapter

Every state manager ultimately has to be reachable through hydrateState or a fallback setState call. Zustand's hook already exposes setState natively, so Flux's default fallback path works on it unmodified — there's nothing an adapter needs to translate.
typescript
1// dispatchToStore's store-write step, unchanged for Zustand:
2
3// hydrateState(store, data) if provided
4// -> else store.setState(data) directly
5// -> else warn and return
6
7// Because 'store' IS the Zustand hook itself, store.setState is
8// always available — Flux never needs an adapter wrapper to call
9// into Zustand safely.
4

Handling sparse updates with hydrateState (alternative)

If you want to keep your Zustand store actions minimal, you can delegate sparse merging and delta frame handling to hydrateState inside the registration instead:
typescript
1// If you prefer a simpler store action without inline delta branching,
2// handle sparse merges inside hydrateState on register():
3
4flux.register({
5 store: useTasksStore,
6 channel: 'tasks',
7 event: 'UPDATE',
8 idbKey: 'tasks:latest',
9 ttl: 'medium',
10 ingestionType: 'COLLECTION_ALL',
11 hydrateState: (store, data) => {
12 if (!data) return
13
14 if (Array.isArray(data)) {
15 store.getState().setTasks(data)
16 return
17 }
18
19 if (typeof data === 'object') {
20 const current = store.getState().tasks
21
22 if (data.op === 'DELETE' || data.eventType === 'DELETE') {
23 const id = String(data.id ?? data.data?.id ?? '')
24 if (id) store.getState().setTasks(current.filter((t) => t.id !== id))
25 return
26 }
27
28 const rawRecord = data.data ?? data.new ?? data
29 const recordId = String(rawRecord?.id ?? data.id ?? '')
30 if (!recordId) return
31
32 const idx = current.findIndex((t) => t.id === recordId)
33 if (idx !== -1) {
34 const next = [...current]
35 next[idx] = { ...next[idx], ...rawRecord, id: recordId }
36 store.getState().setTasks(next)
37 } else {
38 store.getState().setTasks([{ id: recordId, ...rawRecord }, ...current])
39 }
40 }
41 },
42})
5

Combining with scope

Zustand registrations take scope exactly like any other state manager — the store layer is completely independent of multi-tenant scoping.
typescript
1// scope works identically on a Zustand registration as on any
2// other state manager — it lives in the registration/dispatch/IDB
3// layers above the store, not in the store itself:
4
5flux.register({
6 store: useTasksStore,
7 channel: 'tasks',
8 event: 'UPDATE',
9 idbKey: 'tasks:latest',
10 ttl: 'short',
11 scope: 'user',
12 ingestionType: 'COLLECTION_ALL',
13})
6

Zustand vs. other state managers

Zustand is the only state manager Flux treats as native. Everything else uses an explicit adapter wrapper.
typescript
1// State manager support comparison:
2
3// Zustand — native. store.setState(data) called directly, no
4// adapter, no wrapper.
5// Redux — createReduxStoreAdapter(store, actionCreator) or custom adapter
6// Jotai — createJotaiStoreAdapter(atom, store) or custom adapter
7// Stunk — createStunkStoreAdapter(chunk)
8
9// All state managers combine identically with scope, ttl, ingestionType,
10// and every other registration field.

For Redux, see state-managers/redux. For Jotai, see state-managers/jotai. For Stunk, see state-managers/stunk. For every other registration field alongside store, see api/register.