Fluxo UIFluxo UIv0.4.1

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

0
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

0
Saved
This value is persisted to localStorage. Try refreshing the page — the count will be restored!
localStorage["fluxo-store-demo-persist"]
null
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.

Optimistic
0
Server
0
Click +1 to see commits and rollbacks
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.

10
Valid range: 0 to 100

Form Validation

Field-level validation with per-field error messages. Invalid updates are rejected by the middleware.

Current Store State
{
  "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.

Tab ID: MIN9
0
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

Open browser DevTools console to see the logs
Count: 0 · Label: Hello
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 logs

Throttle

Throttle

Batch rapid state updates within a time window using throttleMiddleware

With Throttle (500ms)
0
Renders: 1
Without Throttle
0
Renders: 1
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.

With Debounce (500ms)
Updates: 0
Renders: 1
Without Debounce
Updates: 0
Renders: 1
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..."
    />
  );
}

Build Your Own Middleware

Build Your Own Middleware

The middleware contract is one line. Three patterns cover almost every use case.

The contract

// A middleware is just a function that takes the store and returns a (usually wrapped) store.
import type { Middleware, Store } from 'fluxo-ui/store';

const myMiddleware: Middleware<MyState> = (store: Store<MyState>) => {
  // Inspect, wrap, or augment the store, then return it.
  return store;
};

const store = create<MyState>(() => initial, [myMiddleware]);

Pattern 1 — wrap setState

// Pattern 1: Wrap setState — useful for throttling, validating, transforming writes.
const upperCaseNames: Middleware<{ name: string }> = (store) => {
  const original = store.setState;
  store.setState = (next, ...rest) => {
    if (typeof next !== 'function' && next?.name) next.name = next.name.toUpperCase();
    return original(next as any, ...rest as [any, any?]);
  };
  return store;
};

Pattern 2 — subscribe via store.on()

// Pattern 2: Subscribe via store.on() — useful for logging, persistence, broadcasting.
const auditMiddleware: Middleware<any> = (store) => {
  store.on('change', (state, { previous }) => {
    track('state_changed', { previous, state });
  });
  return store;
};

Pattern 3 — augment the store

// Pattern 3: Augment the returned store with new methods — useful for undo/redo, optimistic, etc.
interface SnapshotStore<T> extends Store<T> {
  snapshot: () => T;
}

const snapshotMiddleware = <T,>(store: Store<T>): SnapshotStore<T> => {
  return {
    ...store,
    snapshot: () => structuredClone(store.getState()),
  };
};

Composition order

// Order matters: middlewares wrap left-to-right.
// The leftmost middleware sees user calls first; the rightmost is closest to the underlying store.
const store = create(() => initial, [
  loggerMiddleware(),       // sees the original setState from the user
  throttleMiddleware(100),  // throttles before the change reaches persist
  persistMiddleware({ debounceMs: 200 }),
]);

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