← Back to blog

Building a Real-Time Order Book in React: A Deep Dive

How to model, merge, and render a live order book from a WebSocket feed - covering data normalisation, price level aggregation, and rendering at 60fps.

By Oliver Benns


The order book is the centrepiece of any trading application. It shows live supply and demand across price levels, updating dozens of times per second. Getting it wrong means laggy UIs, stale prices, and frustrated traders.

This guide walks through the full pipeline: receiving WebSocket messages, merging deltas into state, and rendering the result without dropping frames.

The data model

An order book is two sorted lists - bids (descending by price) and asks (ascending by price). Each entry has a price level and a quantity. Exchange feeds typically send an initial snapshot followed by incremental deltas.

interface PriceLevel { price: number; quantity: number; } interface OrderBook { bids: PriceLevel[]; asks: PriceLevel[]; lastUpdateId: number; }

The key insight is that deltas are upserts: a quantity of zero means "remove this level", any other value replaces the previous quantity at that price.

Receiving and buffering updates

Never pipe raw WebSocket messages directly into React state. A liquid instrument can produce hundreds of updates per second. Instead, buffer updates and flush on a controlled interval.

const bufferRef = useRef<PriceLevel[]>([]); useEffect(() => { const ws = new WebSocket(url); ws.onmessage = (event) => { const delta = JSON.parse(event.data); bufferRef.current.push(...delta.bids, ...delta.asks); }; const interval = setInterval(() => { if (bufferRef.current.length === 0) return; applyDeltas(bufferRef.current); bufferRef.current = []; }, 60); // ~16fps, sufficient for visual updates return () => { ws.close(); clearInterval(interval); }; }, [url]);

This collapses multiple updates to the same price level into a single state change, dramatically reducing render work.

Merging deltas efficiently

The merge function needs to handle insertions, updates, and deletions in a single pass. Using a Map keyed by price gives O(1) lookups.

function mergeDeltas( existing: Map<number, number>, deltas: PriceLevel[] ): Map<number, number> { const merged = new Map(existing); for (const { price, quantity } of deltas) { if (quantity === 0) { merged.delete(price); } else { merged.set(price, quantity); } } return merged; }

Convert back to a sorted array only at the render boundary - keep the internal representation as a Map for fast updates.

Rendering at 60fps

The visual order book typically shows 15–25 price levels per side. Even if the underlying data has hundreds of levels, you only need to render the top N.

const visibleBids = useMemo( () => Array.from(bids.entries()) .sort(([a], [b]) => b - a) .slice(0, 20) .map(([price, quantity]) => ({ price, quantity })), [bids] );

Wrap each row component in React.memo with a custom comparator that checks price and quantity. This prevents re-rendering rows that did not change in the latest flush.

Depth visualisation

The cumulative depth bar is a common visual element. Calculate it as a percentage of the maximum cumulative quantity across visible levels.

const cumulativeBids = visibleBids.reduce<number[]>((acc, level, i) => { acc.push((acc[i - 1] ?? 0) + level.quantity); return acc; }, []); const maxCumulative = Math.max( cumulativeBids[cumulativeBids.length - 1] ?? 0, cumulativeAsks[cumulativeAsks.length - 1] ?? 0 );

Render the bar as a CSS background gradient or an absolutely positioned div. Avoid SVG for this - it is slower to update at high frequencies.

Handling reconnections

WebSocket connections drop. When reconnecting, you must re-request the full snapshot and discard any buffered deltas from before the reconnection. A sequence number (lastUpdateId) from the exchange feed lets you detect gaps.

if (delta.firstUpdateId > lastUpdateId + 1) { // Gap detected - request fresh snapshot requestSnapshot(); return; }

Performance checklist

  • Buffer WebSocket messages outside React state
  • Flush on a 50–100ms interval, not on every message
  • Store order book as a Map, not a sorted array
  • Only sort and slice for rendering
  • Memoize row components with custom equality checks
  • Use CSS for depth bars, not SVG
  • Handle reconnection and sequence gaps gracefully

The order book is a microcosm of every real-time rendering challenge in a trading app. Get this component right and the same patterns - buffering, merging, selective rendering - apply everywhere else in your application.

Kickstart Your Trading Application

Hedge UI is a React starter kit with production-ready trading components, real-time data handling, and customisable layouts — so you can ship faster.

Get Hedge UI