Skip to content

Cookbook: Practical Patterns

The snippets in this page are copy-pasteable starting points for common scenarios. They are all framework-agnostic vanilla TS/JS and assume you are using the APIs from @yiin/reactive-proxy-state 1.x.

Each snippet is intentionally concise; production code should add error handling and edge-case guards as appropriate.


1. Off-thread Image Processing (Worker Sync)

An in-browser image editor often needs CPU-intensive work—flattening layers, computing histograms, applying filters—without freezing the UI. We keep a single reactive document on the main thread and mirror it inside a Web Worker. Every mutation ships as a tiny StateEvent.

ts
// main.ts  (UI thread)
import { reactive, updateState, StateEvent } from "@yiin/reactive-proxy-state";

const worker = new Worker(new URL("./worker.ts", import.meta.url), {
  type: "module",
});

export const doc = reactive(
  {
    layers: [
      { id: 1, pixels: new Uint8ClampedArray() },
      { id: 2, pixels: new Uint8ClampedArray() },
    ],
    activeLayer: 1,
    zoom: 1,
  },
  (ev) => worker.postMessage(ev) // forward every mutation to the worker
);

// Apply mutations coming back from the worker
worker.onmessage = (e: MessageEvent<StateEvent>) => updateState(doc, e.data);

// Receive computed artefacts (e.g. histogram) and render them
worker.addEventListener("message", (e) => {
  if (e.data?.type === "histogram") {
    renderHistogram(e.data.data);
  }
});
ts
// worker.ts
import {
  reactive,
  updateState,
  StateEvent,
  watchEffect,
} from "@yiin/reactive-proxy-state";

const workerDoc = reactive({}, (ev) => self.postMessage(ev)); // send worker-originated changes back

self.onmessage = (e: MessageEvent<StateEvent>) =>
  updateState(workerDoc, e.data);

// Re-compute heavy stuff whenever the document changes
watchEffect(() => {
  const flattened = flattenLayers(workerDoc.layers);
  const histogram = makeHistogram(flattened);
  self.postMessage({ type: "histogram", data: histogram });
});

function flattenLayers(layers: any[]) {
  /* heavy pixel blending */
}
function makeHistogram(pixels: Uint8ClampedArray) {
  /* heavy stats */
}

Why this works
StateEvents are minimal diffs that move quickly across postMessage, while the expensive math lives entirely off the render thread.


2. Persistent LocalStorage Store

Automatically save the entire state tree to localStorage on every mutation and restore it on startup.

ts
import {
  reactive,
  updateState,
  StateEvent,
  deepClone,
} from "@yiin/reactive-proxy-state";

const STORAGE_KEY = "my-app-state";

// ① Load previous snapshot (if any)
const saved = localStorage.getItem(STORAGE_KEY);
const initial = saved ? JSON.parse(saved) : { todos: [], theme: "light" };

// ② Create reactive store with emitter
export const store = reactive(initial, (ev) => persist(ev));

function persist(ev: StateEvent) {
  // We only need the *final* object; deepClone to avoid mutating proxies
  const snapshot = deepClone(store);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
}

Notes

  1. Serialising on every mutation is fine for small/medium trees. For very large trees throttle/debounce persist.
  2. deepClone removes proxy wrappers so the data is safe to serialise.

3. Time-Travel Undo / Redo

Record StateEvents in two stacks and replay them via updateState.

ts
import { reactive, updateState, StateEvent } from "@yiin/reactive-proxy-state";

const undoStack: StateEvent[] = [];
const redoStack: StateEvent[] = [];

export const doc = reactive({ text: "" }, (ev) => {
  undoStack.push(ev);
  // Clearing redoStack because new branch of history starts
  redoStack.length = 0;
});

export function undo() {
  const last = undoStack.pop();
  if (!last) return;
  // Reverse the change (simple strategy: swap old/new + action)
  const inverse = invertEvent(last);
  redoStack.push(inverse);
  updateState(doc, inverse);
}

export function redo() {
  const next = redoStack.pop();
  if (!next) return;
  undoStack.push(next);
  updateState(doc, next);
}

function invertEvent(ev: StateEvent): StateEvent {
  switch (ev.action) {
    case "set":
      return { ...ev, newValue: ev.oldValue, oldValue: ev.newValue };
    case "delete":
      return { action: "set", path: ev.path, newValue: ev.oldValue };
    case "array-push":
      return { action: "array-pop", path: ev.path };
    // handle other actions as needed …
    default:
      throw new Error(`Cannot invert ${ev.action}`);
  }
}

Tip For complex apps you'll likely serialise events to disk and prune history to keep memory usage predictable.


Where to go from here

  • Share your own patterns in issues/PRs – the cookbook is intended to grow.
  • Combine patterns: e.g. worker sync plus localStorage persistence for offline-first behaviour.