State Managers

Redux

Redux integrates through createReduxStoreAdapter or a custom adapter object — bridging dispatchToStore's setState fallback to Redux's action dispatch model.

Prerequisites
  • A Redux store from configureStore() and action creators from createSlice() — see Redux Toolkit's docs if you're new to it
  • Familiarity with register() — see api/register
1

A plain Redux slice and store

Define a slice with reducers for full array replacements (setItems), optimistic placeholder additions (addOptimisticItem), single confirmed item upserts (upsertItem), and removals (removeItem):
typescript
1// A Redux Toolkit slice with full support for array hydrations,
2// optimistic placeholders, and single live delta upserts/deletions.
3import { createSlice, configureStore, type PayloadAction } from '@reduxjs/toolkit'
4
5export interface Item {
6 id: string
7 name: string
8 description: string
9 status: string
10 price: number
11 createdAt: string
12 updatedAt: string
13}
14
15interface ItemsState {
16 list: Item[]
17}
18
19const initialState: ItemsState = { list: [] }
20
21const itemsSlice = createSlice({
22 name: 'items',
23 initialState,
24 reducers: {
25 // Full-array replace — IDB cold-boot and bootstrap dispatch
26 setItems(state, action: PayloadAction<Item[]>) {
27 state.list = Array.isArray(action.payload) ? action.payload : []
28 },
29 addOptimisticItem(state, action: PayloadAction<Item>) {
30 state.list.unshift(action.payload)
31 },
32 // Upsert a single confirmed item — replaces optimistic placeholder by name
33 // or patches existing by id (sparse update), or appends if not found
34 upsertItem(state, action: PayloadAction<Item>) {
35 const incoming = action.payload
36 const optimisticIdx = state.list.findIndex(
37 (item) =>
38 item.status === 'optimistic_pending' &&
39 item.name === incoming.name
40 )
41 if (optimisticIdx !== -1) {
42 state.list[optimisticIdx] = incoming
43 return
44 }
45 const existingIdx = state.list.findIndex((item) => item.id === incoming.id)
46 if (existingIdx !== -1) {
47 state.list[existingIdx] = { ...state.list[existingIdx], ...incoming }
48 return
49 }
50 state.list.unshift(incoming)
51 },
52 removeItem(state, action: PayloadAction<string>) {
53 state.list = state.list.filter((item) => item.id !== action.payload)
54 },
55 },
56})
57
58export const { setItems, addOptimisticItem, upsertItem, removeItem } = itemsSlice.actions
59
60export const reduxStore = configureStore({
61 reducer: { items: itemsSlice.reducer },
62})
2

Registering with createReduxStoreAdapter

For simple stores that process whole array payloads, wrap the store and action creator with createReduxStoreAdapter:
typescript
1import { createReduxStoreAdapter } from '@tsworldtech/flux'
2import { reduxStore, setItems } from './store'
3
4flux.register({
5 store: createReduxStoreAdapter(reduxStore, setItems),
6 channel: 'items',
7 event: 'UPDATE',
8 idbKey: 'items:latest',
9 ttl: 'long',
10 ingestionType: 'COLLECTION_ALL',
11})
12
13// createReduxStoreAdapter wraps store.dispatch(actionCreator(data))
14// behind a setState(data)-shaped call for standard array payloads.
3

Why Redux needs an adapter

A Redux store only ever changes through dispatch() — there's no native setState method. The adapter exists to give dispatchToStore's fallback path a concrete function to invoke.
typescript
1// A Redux store has no setState of its own — the only sanctioned
2// way to change state is store.dispatch(someAction), never a
3// direct mutation. dispatchToStore's fallback path
4// (store.setState(data)) has nothing to call on a raw Redux store,
5// so Redux needs an adapter to bridge the two:
6
7// hydrateState(store, data) if provided
8// -> else store.setState(data) — this is what the adapter exists
9// to make possible on top of store.dispatch()
10// -> else warn and return
11
12// createReduxStoreAdapter or a custom adapter object wraps
13// store.dispatch(...) behind a setState(data)-shaped signature.
4

Handling sparse updates & single delta events

When streaming live updates over WebSocket, SSE, or Socket.IO, your backend streams single delta frames (CREATE, sparse UPDATE, and DELETE) alongside initial array hydrations. Use a hand-written adapter object to route incoming data to the correct Redux action:
typescript
1// When your realtime backend streams single delta frames (CREATE,
2// sparse UPDATE, or DELETE) alongside full array hydration payloads,
3// write a custom Redux adapter object to dispatch the appropriate action:
4
5export const itemsAdapter = {
6 getState: () => reduxStore.getState().items.list,
7 setState: (data: any) => {
8 if (!data) return
9
10 // ── 1. Array payload — IDB cold-boot, bootstrap, or full batch sync ──
11 if (Array.isArray(data)) {
12 const current = reduxStore.getState().items.list
13 const hasOptimistic = current.some((i) => i.status === 'optimistic_pending')
14
15 if (hasOptimistic) {
16 const resolved = data.map((item) => {
17 if (item.status !== 'optimistic_pending') return item
18 const confirmed = data.find(
19 (i) => i.status !== 'optimistic_pending' && i.name === item.name
20 )
21 return confirmed ? null : item
22 }).filter(Boolean) as Item[]
23
24 reduxStore.dispatch(setItems(resolved))
25 return
26 }
27
28 reduxStore.dispatch(setItems(data))
29 return
30 }
31
32 // ── 2. Object payload — single live delta event or backfill frame ──
33 if (typeof data === 'object') {
34 // DELETE frame
35 if (data.op === 'DELETE' || data.eventType === 'DELETE') {
36 const idToRemove = String(data.id ?? data._id ?? data.data?.id ?? '')
37 if (idToRemove) reduxStore.dispatch(removeItem(idToRemove))
38 return
39 }
40
41 // Extract record payload & ID
42 const rawRecord = data.data ?? data.new ?? data
43 const recordId = String(rawRecord?.id ?? rawRecord?._id ?? data.id ?? '')
44
45 if (recordId) {
46 const normalizedItem = { id: recordId, ...rawRecord } as Item
47 reduxStore.dispatch(upsertItem(normalizedItem))
48 }
49 }
50 },
51}
5

Reaching for hydrateState

If you use createReduxStoreAdapter but need to dispatch multiple actions or perform side effects upon payload arrival, pass a custom hydrateState callback during registration:
typescript
1// Alternatively, use hydrateState inside register() to handle
2// multi-action dispatches or custom payload reshaping:
3
4flux.register({
5 store: createReduxStoreAdapter(reduxStore, setItems),
6 channel: 'items',
7 event: 'UPDATE',
8 idbKey: 'items:latest',
9 ttl: 'long',
10 ingestionType: 'COLLECTION_ALL',
11 hydrateState: (store, data) => {
12 if (Array.isArray(data)) {
13 store.setState(data)
14 } else if (typeof data === 'object') {
15 const record = data.data ?? data
16 reduxStore.dispatch(upsertItem(record))
17 }
18 // dispatch an additional side-effect action
19 reduxStore.dispatch(recordLastSyncedAt(Date.now()))
20 },
21})
typescript
1// createReduxStoreAdapter is built around a single action creator —
2// it calls store.dispatch(actionCreator(data)) with the raw payload.
3// For complex stores that process both array hydrations and single live
4// delta updates, use a hand-written adapter object (Step 4 above).
Need multiple actions from one channel?

createReduxStoreAdapter is deliberately single-action. If a channel needs to fan out to more than one reducer, use hydrateState or a custom adapter object as shown above.

6

Combining with scope

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

Redux vs. other state managers

Redux is one of two state managers that benefit from an explicit adapter factory — the other being Jotai.
typescript
1// State manager support:
2
3// Zustand — native. store.setState(data) called directly, no
4// adapter, no wrapper.
5// Redux — createReduxStoreAdapter(store, actionCreator) or custom adapter object
6// Jotai — createJotaiStoreAdapter(atom, store) or custom adapter object
7// Stunk — createStunkStoreAdapter(chunk)
8
9// All state managers combine identically with scope, ttl, ingestionType,
10// and every other registration field — the adapter layer is
11// unaware of scoping, which lives entirely above it.

For Zustand, Flux's native option, see state-managers/zustand. For Jotai, see state-managers/jotai. For Stunk, see state-managers/stunk. For every other registration field alongside store, see api/register.