Store Middleware
Composable middleware functions that extend store behavior. Add undo/redo, persistence, validation, logging, and throttling with a single line.
Undo / Redo
Undo / Redo
Track state history and navigate back and forth with undoRedoMiddleware
import { create, createHook } from 'fluxo-ui/store';
import { undoRedoMiddleware } from 'fluxo-ui/store/middlewares';
import type { UndoRedoStateProps, UndoRedoStore } from 'fluxo-ui/store/middlewares';
interface CounterState { value: number; }
const store = create<CounterState>(
() => ({ value: 0 }),
[undoRedoMiddleware({ maxHistorySize: 20 })]
);
const useStore = createHook<CounterState, CounterState & UndoRedoStateProps>(store);
const typedStore = store as UndoRedoStore<CounterState>;
function UndoDemo() {
const { value, canUndo, canRedo } = useStore();
return (
<div>
<span>{value}</span>
<Button label="+1"
onClick={() => store.setState(s => ({ value: s.value + 1 }))} />
<Button label="+5"
onClick={() => store.setState(s => ({ value: s.value + 5 }))} />
<Button label="Undo" disabled={!canUndo}
onClick={() => typedStore.undo()} />
<Button label="Redo" disabled={!canRedo}
onClick={() => typedStore.redo()} />
</div>
);
}Persistence
Persistence
Automatically save and restore state from localStorage with persistMiddleware
import { create, createHook } from 'fluxo-ui/store';
import { persistMiddleware } from 'fluxo-ui/store/middlewares';
const store = create<{ count: number }>(
() => ({ count: 0 }),
[persistMiddleware({ storage: 'local', key: 'my-app-counter' })]
);
const useStore = createHook(store);
function PersistDemo() {
const { count } = useStore();
return (
<div>
<span>Persisted count: {count}</span>
<Button label="Increment"
onClick={() => store.setState(s => ({ count: s.count + 1 }))} />
<Button label="Refresh Page"
onClick={() => window.location.reload()} />
</div>
);
}Optimistic Updates
Optimistic Updates with Auto-Rollback
Click +1 — UI updates immediately. A simulated server fails ~40% of the time. On failure the change rolls back automatically.
import { create } from 'fluxo-ui/store';
import { optimisticMiddleware } from 'fluxo-ui/store/middlewares';
const store = create<LikeState>(
() => ({ likes: 0, serverLikes: 0 }),
[optimisticMiddleware<LikeState>({
commit: async (next) => {
// Send to server. If this rejects, the state automatically rolls back.
await fetch('/api/likes', { method: 'POST', body: JSON.stringify(next) });
store.setState({ serverLikes: next.likes });
},
onRollback: (prev, attempted, error) => {
showSnackbar({ severity: 'error', message: 'Save failed — reverted' });
},
})]
);
// Use store.optimistic() instead of setState() for changes that need server confirmation
store.optimistic((s) => ({ likes: s.likes + 1 }));
Validation
Basic Validation
Reject invalid state updates with validationMiddleware. Invalid setState calls are blocked entirely.
Form Validation
Field-level validation with per-field error messages. Invalid updates are rejected by the middleware.
{
"name": "",
"email": "",
"age": 0
}import { create, createHook } from 'fluxo-ui/store';
import { validationMiddleware } from 'fluxo-ui/store/middlewares';
interface FormState {
name: string;
email: string;
age: number;
}
type FormErrors = Partial<Record<keyof FormState, string>>;
const validationErrors: { current: FormErrors } = { current: {} };
const formStore = create<FormState>(
() => ({ name: '', email: '', age: 0 }),
[validationMiddleware<FormState>({
validator: (state) => {
const errors: FormErrors = {};
if (state.name.length > 0 && state.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
}
if (state.email.length > 0 &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(state.email)) {
errors.email = 'Please enter a valid email address';
}
if (state.age < 0) errors.age = 'Age cannot be negative';
if (state.age > 150) errors.age = 'Age cannot exceed 150';
return Object.keys(errors).length > 0 ? errors : undefined;
},
onValidationError: (errors) => {
validationErrors.current = errors as FormErrors;
}
})]
);
const useFormStore = createHook(formStore);
function FormDemo() {
const { name, email, age } = useFormStore();
const [errors, setErrors] = useState<FormErrors>({});
const updateField = (update: Partial<FormState>) => {
validationErrors.current = {};
formStore.setState(update);
requestAnimationFrame(() => {
setErrors({ ...validationErrors.current });
});
};
return (
<form>
<TextInput label="Name" value={name}
onChange={v => updateField({ name: v })} />
{errors.name && <span className="error">{errors.name}</span>}
<TextInput label="Email" value={email}
onChange={v => updateField({ email: v })} />
{errors.email && <span className="error">{errors.email}</span>}
<TextInput label="Age" type="number" value={String(age)}
onChange={v => updateField({ age: Number(v) })} />
{errors.age && <span className="error">{errors.age}</span>}
</form>
);
}Sync
Sync Middleware (Pluggable Transport)
syncMiddleware abstracts the transport. Below uses BroadcastChannel; the same store can swap to WebSocket or a custom transport without changing call sites. Open the page in two tabs to see updates flow through.
import { create } from 'fluxo-ui/store';
import {
syncMiddleware,
broadcastChannelTransport,
webSocketTransport,
storageEventTransport,
} from 'fluxo-ui/store/middlewares';
const store = create<SyncState>(() => ({ count: 0 }), [
syncMiddleware<SyncState>({
transport: broadcastChannelTransport('my-app'), // or webSocketTransport('wss://...')
resolve: 'merge', // 'remote-wins' | 'local-wins' | 'merge' | (local, remote) => Partial<T>
}),
]);
// Implement your own SyncTransport to use any other channel:
// { send(msg), onReceive(handler): unsubscribe, close?() }
Logger
Logger
Logs state changes to the browser console with loggerMiddleware
import { create, createHook } from 'fluxo-ui/store';
import { loggerMiddleware } from 'fluxo-ui/store/middlewares';
const store = create<{ count: number; label: string }>(
() => ({ count: 0, label: 'Hello' }),
[loggerMiddleware()]
);
const useStore = createHook(store);
// With predicate — only log when count changes
const store2 = create<{ count: number }>(
() => ({ count: 0 }),
[loggerMiddleware((state, previous) => state.count !== previous?.count)]
);
// Open browser DevTools console to see logsThrottle
Throttle
Batch rapid state updates within a time window using throttleMiddleware
import { create, createHook } from 'fluxo-ui/store';
import { throttleMiddleware } from 'fluxo-ui/store/middlewares';
// Updates are batched within a 500ms window
const store = create<{ value: number }>(
() => ({ value: 0 }),
[throttleMiddleware(500)]
);
const useStore = createHook(store);
function ThrottleDemo() {
const { value } = useStore();
const handleRapidClicks = () => {
// These rapid calls are merged and applied once after 500ms
for (let i = 0; i < 10; i++) {
store.setState((s) => ({ value: s.value + 1 }));
}
};
return (
<div>
<span>Value: {value}</span>
<Button label="Rapid +10" onClick={handleRapidClicks} />
</div>
);
}Debounce
Debounce
Delay state updates until input activity stops. Compare debounced vs immediate updates.
import { create, createHook } from 'fluxo-ui/store';
import { debounceMiddleware } from 'fluxo-ui/store/middlewares';
const store = create<{ query: string }>(
() => ({ query: '' }),
[debounceMiddleware(500)]
);
const useStore = createHook(store);
function SearchInput() {
const { query } = useStore();
// State only updates 500ms after the user stops typing
return (
<TextInput
value={query}
onChange={(e) => store.setState({ query: e.target.value })}
placeholder="Type to search..."
/>
);
}Import
import {
persistMiddleware,
undoRedoMiddleware,
optimisticMiddleware,
validationMiddleware,
syncMiddleware,
broadcastChannelTransport,
webSocketTransport,
storageEventTransport,
loggerMiddleware,
throttleMiddleware,
debounceMiddleware,
devToolsMiddleware,
immerMiddleware,
} from 'fluxo-ui/store/middlewares';Features
Undo / Redo
Navigate state history with configurable max history size and one-call undo/redo
Persistence
Auto-save state to localStorage or sessionStorage with a configurable key
Validation
Intercept setState calls and reject updates that fail validation rules
Logger
Log state changes to the console with optional predicate filtering
Throttle
Batch rapid setState calls within a configurable delay window to reduce noise
Debounce
Delay state updates until activity stops — ideal for search inputs and auto-save
Optimistic Updates
Apply changes locally, await an async commit, and roll back automatically when the commit rejects
Pluggable Sync
syncMiddleware accepts any transport — BroadcastChannel, storage event, WebSocket, or your own
Composable
Middlewares are composable — stack multiple middlewares to combine behaviors