Skip to main content
Version: v8.0.0

Throttling & Batching in React

With useListener, every delivered message is a state update, and every state update is a render. On a stream that pushes hundreds of messages per second, that is hundreds of renders per second for a screen that refreshes sixty times.

The fix lives on the listener, not in the component. Configure a delivery strategy once, and useListener simply receives fewer, better-shaped updates. No hook options, no manual throttling in effects.


What you'll learn
  1. How a latest listener turns a price firehose into one render per frame.
  2. How a batch listener delivers arrays so you can append many rows in one state update.
  3. How to let the user choose the refresh rate with setDelivery and useMemo.
  4. The one gotcha with custom strategies and React re-renders.

Start with the framework-agnostic Throttling & Batching guide if you have not read it - this page focuses on the React side.


Sampling a price ticker​

Define the listener with a latest strategy. useListener needs no changes: data is simply the newest price in the last window.

api/socket.ts
export const onPrice = socket.createListener<Price>()({
topic: "prices",
delivery: { strategy: "latest", interval: 16 },
});
PriceTicker.tsx
import { useListener } from "@hyper-fetch/react";
import { onPrice } from "./api/socket";

export function PriceTicker() {
// Renders at most ~60 times per second regardless of how fast the server sends.
const { data, connected } = useListener(onPrice);

if (!connected) return <span>Connecting…</span>;
return <span>{data ? data.value.toFixed(2) : "—"}</span>;
}

Batching a trade log​

With batch, data and the onEvent payload are arrays, typed automatically as Trade[]. Append the whole batch in a single state update.

api/socket.ts
export const onTrade = socket.createListener<Trade>()({
topic: "trades",
delivery: { strategy: "batch", interval: 100, maxSize: 500 },
});
TradeLog.tsx
import { useState } from "react";
import { useListener } from "@hyper-fetch/react";
import { onTrade } from "./api/socket";

export function TradeLog() {
const [trades, setTrades] = useState<Trade[]>([]);
const { onEvent } = useListener(onTrade);

onEvent(({ data }) => {
// `data` is Trade[] - one setState per window instead of one per trade.
setTrades((previous) => [...previous, ...data].slice(-1000));
});

return (
<ul>
{trades.map((trade) => (
<li key={trade.id}>
{trade.symbol} {trade.price}
</li>
))}
</ul>
);
}

Letting the user pick the refresh rate​

setDelivery returns a new listener. Memoize it on the user's choice and pass it to useListener; the hook re-subscribes when the delivery changes and disposes the previous window.

PriceTicker.tsx
import { useMemo, useState } from "react";
import { useListener } from "@hyper-fetch/react";
import { onPrice } from "./api/socket";

export function PriceTicker() {
const [rate, setRate] = useState(16);
// A new listener only when `rate` changes - not on every render.
const live = useMemo(() => onPrice.setDelivery({ strategy: "latest", interval: rate }), [rate]);
const { data } = useListener(live);

return (
<>
<select value={rate} onChange={(event) => setRate(Number(event.target.value))}>
<option value={16}>Realtime</option>
<option value={250}>Relaxed</option>
<option value={1000}>Slow</option>
</select>
<span>{data?.value ?? "—"}</span>
</>
);
}

The gotcha: custom strategies and identity​

useListener decides whether to re-subscribe by comparing the listener's topic, options and delivery:

  • Preset objects ({ strategy: "latest", interval: 16 }) are compared by value. Writing them inline is fine.
  • Custom strategy functions are compared by identity. Define them at module level, next to your listeners. A strategy created inside a component is a new function on every render and would re-subscribe each time, dropping whatever was buffered.
api/socket.ts
// ✅ module level - stable identity
export const summarize = createDeliveryStrategy<Trade, Summary>(/* ... */);
export const onTradeSummary = socket.createListener<Trade>()({ topic: "trades", delivery: summarize });

Congratulations!

You can now keep React UIs smooth on the busiest streams.

  • A latest listener caps renders at the rate you choose.
  • A batch listener gives you arrays and one state update per window.
  • setDelivery + useMemo lets the user control the rate.
  • Custom strategies live at module level to keep a stable identity.
useListener
Full reference for the useListener hook.
Throttling & Batching
The framework-agnostic guide with timelines, semantics and custom strategies.