esc

Type to search...

Backpressure

A high-frequency stream that triggers a state update per event will melt your UI thread. Quote feeds, stream tokens, telemetry, multiplayer cursors — anything that arrives faster than React can paint — needs batching at the receive boundary.

useSocketEventBatch buffers events that match a discriminator value and flushes the batch to your handler on a fixed interval. One flush, one render.

Usage

PriceTicker.tsx
import { useSocketEventBatch } from "@luciodale/react-socket";

function PriceTicker() {
  const [latest, setLatest] = useState<Record<string, number>>({});

  useSocketEventBatch(
    manager,
    "tick",
    (msgs) => {
      // One setState per flush, no matter how many ticks arrived.
      setLatest((prev) => {
        const next = { ...prev };
        for (const m of msgs) next[m.symbol] = m.price;
        return next;
      });
    },
    { flushMs: 100 },
  );

  return <PriceGrid prices={latest} />;
}

Why this matters

Without batching, every event causes a re-render. With it, you cap renders at 1000 / flushMs:

example.tsx
// Without batching: one re-render per tick. With 200 ticks per
// second, that is 200 renders per second. The component dies.
useSocketEvent(manager, "tick", (msg) => {
  setLatest((prev) => ({ ...prev, [msg.symbol]: msg.price }));
});

// With batching: one re-render per flush window. With flushMs: 100,
// that is 10 renders per second regardless of tick rate.
useSocketEventBatch(
  manager,
  "tick",
  (msgs) => setLatest((prev) => mergeTicks(prev, msgs)),
  { flushMs: 100 },
);

Picking flushMs

  • 16ms (~60 renders / sec): feels live. Use for cursors, scroll, or anything that should look continuous.
  • 50–100ms (10–20 / sec): feels responsive but cheaper. Good for price tickers, dashboards.
  • 250ms+ (≤ 4 / sec): clearly batched. Use for activity feeds where order matters but freshness does not.

Order is preserved

Events inside a batch arrive in the order the server sent them. Use this for stream-delta or any other accumulating sequence:

streaming.tsx
useSocketEventBatch(
  manager,
  "stream-delta",
  (msgs) => {
    // Order is preserved within a batch, exactly as received.
    for (const m of msgs) appendDelta(m.id, m.delta);
  },
  { flushMs: 16 },
);

Trailing-latency: idleMs

flushMs alone produces a visible stall at the end of a stream: the last few events sit in the buffer waiting for the next interval tick. Pass idleMs alongside it and the batch also flushes after that many ms of silence on the channel. Typical pairing for an LLM token stream: { flushMs: 16, idleMs: 8 } so the final tokens render without a perceived hang.

streaming-idle.tsx
// Pair flushMs with idleMs to flush early when the stream goes quiet.
// Without idleMs, the last 1-3 tokens of an LLM stream sit in the buffer
// waiting for the next interval tick, producing a visible stall.
useSocketEventBatch(
  manager,
  "stream-delta",
  (msgs) => {
    for (const m of msgs) appendDelta(m.id, m.delta);
  },
  { flushMs: 16, idleMs: 8 },
);

What it does NOT do

  • It does not coalesce duplicates. If the server sends three updates for the same key in one window, your handler sees three entries. Dedupe in the handler.
  • It does not flush on unmount. Pending events in the buffer at unmount time are dropped. Persist anything you cannot afford to lose before unmounting.
  • It does not signal upstream. Truly congested servers need server-side flow control; client-side batching only protects the renderer.

When to skip it

  • Your stream is slow. If events arrive once a second, the per-event hook is fine and simpler.
  • Each event must update independent UI atomically. Batching collapses N events into one render — not the model you want for confirm-in-place flows.
  • You are using an external store (zustand, redux) that already coalesces. The store deals with churn; the React tree only renders on selector changes.

Next steps

  • API referenceuseSocketEventBatch signature
  • Binary frames — pair batching with binary for high-throughput streams
  • Testing — asserting on batch flushes with fake timers