A trading surface is not a dashboard with a Buy button glued on. Prices, inventory, and counterparty state arrive as a firehose. The purchase path has to feel like a form: one decision, one confirmation, no flicker.
Split the streams
Treat inbound events as at least two products:
- Signal — prices, depth, presence. High frequency. Lossy is allowed if the latest value wins.
- Commit — cart, quote lock, payment intent. Low frequency. Must be exact.
Those streams should not share a React context at the top of the tree. Signal can live in a store that components subscribe to by selector. Commit should be props and server state.
type MarketTick = {
sku: string;
bid: number;
ask: number;
ts: number;
};
type CheckoutSnapshot = {
quoteId: string;
sku: string;
lockedAsk: number;
expiresAt: number;
};Batch before you render
WebSocket messages are cheaper than frames. If you setState per tick, React will spend the session reconciling work the user cannot perceive.
The pattern that survived production load is a rAF-aligned batcher: collect ticks for the frame, fold by sku, then publish one immutable map.
export function createTickBatcher(publish: (snapshot: Map<string, MarketTick>) => void) {
const pending = new Map<string, MarketTick>();
let frame = 0;
return function ingest(tick: MarketTick) {
pending.set(tick.sku, tick);
if (frame) return;
frame = requestAnimationFrame(() => {
publish(new Map(pending));
pending.clear();
frame = 0;
});
};
}Latest-write-wins is correct for a book. It is wrong for fills and invoices. Do not reuse this helper on commit events.
Keep checkout off the hot path
The buy flow should read a locked quote, not the live ask. Lock on intent, show a countdown, and refuse to bind the submit button to the tick store.
A practical UI split:
- Ticker and depth subscribe to the batcher.
- Quote lock is a server action with an expiry.
- Toasts and banners are a third lane so errors do not steal layout from the form.
What to measure
If you cannot see these, you are guessing:
- Time from tick arrival to paint, p95
- Checkout renders per minute while the book is live
- Quote lock mismatch rate (UI ask vs server ask at submit)
The architecture is working when the book looks alive and the purchase form looks bored.
Takeaway
Realtime UI is a scheduling problem. Give noise a cheap lane, give money a quiet lane, and never let them share a render.