How I Designed a Real-Time Trading Dashboard Using React + WebSockets
A deep dive into building a high-performance trading dashboard with React, WebSockets, and optimized state management for handling thousands of updates per second.

The Challenge: Real-Time at Scale
Building a trading dashboard that feels responsive while processing hundreds of price updates per second is a masterclass in performance optimization. Unlike typical CRUD apps, every millisecond of latency is visible to users watching live markets.
This article breaks down the architecture, patterns, and optimizations I used to build a dashboard that traders rely on for split-second decisions—without dropping frames or missing data.
Architecture Overview: Data Flow
The system has three critical data pipelines:
Exchange WebSocket → Web Worker → React State → UI
↓
IndexedDB (buffer)Key decisions:
- Web Worker: Processes and aggregates market data off the main thread
- IndexedDB: Buffers historical data for chart rendering
- State management: Zustand for global state, React Query for API data
WebSocket Connection Management
Reliable WebSocket connections are non-negotiable. This custom hook handles connection, reconnection, and message routing:
import { useEffect, useRef, useState } from 'react'
interface UseWebSocketOptions {
url: string
onMessage: (data: any) => void
reconnectInterval?: number
maxReconnectAttempts?: number
}
export function useWebSocket({
url,
onMessage,
reconnectInterval = 3000,
maxReconnectAttempts = 10,
}: UseWebSocketOptions) {
const ws = useRef<WebSocket | null>(null)
const [isConnected, setIsConnected] = useState(false)
const [reconnectCount, setReconnectCount] = useState(0)
const reconnectTimeoutRef = useRef<NodeJS.Timeout>()
useEffect(() => {
function connect() {
ws.current = new WebSocket(url)
ws.current.onopen = () => {
setIsConnected(true)
setReconnectCount(0)
console.log('✅ WebSocket connected')
}
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data)
onMessage(data)
}
ws.current.onerror = (error) => {
console.error('WebSocket error:', error)
}
ws.current.onclose = () => {
setIsConnected(false)
// Auto-reconnect with exponential backoff
if (reconnectCount < maxReconnectAttempts) {
const delay = reconnectInterval * Math.pow(1.5, reconnectCount)
console.log(`Reconnecting in ${delay}ms...`)
reconnectTimeoutRef.current = setTimeout(() => {
setReconnectCount(prev => prev + 1)
connect()
}, delay)
}
}
}
connect()
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
ws.current?.close()
}
}, [url, reconnectCount])
const send = (data: any) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(data))
}
}
return { isConnected, send }
}This hook automatically reconnects with exponential backoff, preventing server overload during outages.
Offloading to Web Workers
Processing raw market data on the main thread causes jank. Offload it to a Web Worker:
// market-worker.ts
self.onmessage = (event) => {
const { type, data } = event.data
if (type === 'PRICE_UPDATE') {
// Aggregate price updates
const processed = aggregateTicks(data)
self.postMessage({ type: 'AGGREGATED_PRICES', data: processed })
}
if (type === 'ORDERBOOK_UPDATE') {
// Calculate orderbook depth
const depth = calculateOrderbookDepth(data)
self.postMessage({ type: 'ORDERBOOK_DEPTH', data: depth })
}
}
function aggregateTicks(ticks: any[]) {
// Implementation: Group ticks by symbol, calculate OHLCV
return ticks.reduce((acc, tick) => {
// ... aggregation logic
}, {})
}React component usage:
const worker = useMemo(() => new Worker(new URL('./market-worker', import.meta.url)), [])
useEffect(() => {
worker.onmessage = (event) => {
if (event.data.type === 'AGGREGATED_PRICES') {
setPrices(event.data.data)
}
}
}, [worker])State Management: Zustand for Performance
Zustand outperforms Redux for high-frequency updates due to its subscription model:
import create from 'zustand'
interface MarketState {
prices: Record<string, number>
updatePrice: (symbol: string, price: number) => void
positions: Position[]
updatePosition: (position: Position) => void
}
export const useMarketStore = create<MarketState>((set) => ({
prices: {},
updatePrice: (symbol, price) =>
set((state) => ({
prices: { ...state.prices, [symbol]: price },
})),
positions: [],
updatePosition: (position) =>
set((state) => ({
positions: state.positions.map(p =>
p.id === position.id ? position : p
),
})),
}))Components subscribe only to the state they need:
function PriceDisplay({ symbol }: { symbol: string }) {
const price = useMarketStore((state) => state.prices[symbol])
return <div>{price}</div>
}This prevents unnecessary re-renders when other prices update.
Optimizing Re-Renders with React.memo
Every component in the ticker list needs React.memo to prevent cascade re-renders:
const TickerRow = React.memo(({ symbol, price, change }: TickerProps) => {
return (
<div className="ticker-row">
<span>{symbol}</span>
<span className={change >= 0 ? 'green' : 'red'}>
{price.toFixed(2)}
</span>
</div>
)
}, (prev, next) => {
// Custom comparison: only re-render if price actually changed
return prev.price === next.price && prev.change === next.change
})Virtualization for Large Lists
Rendering 500+ tickers? Use react-window for virtualization:
import { FixedSizeList } from 'react-window'
function TickerList({ tickers }: { tickers: Ticker[] }) {
return (
<FixedSizeList
height={600}
itemCount={tickers.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<TickerRow {...tickers[index]} />
</div>
)}
</FixedSizeList>
)
}This renders only visible rows, keeping 60fps even with thousands of items.
Real-Time Charts with Chart.js
For candlestick charts, integrate Chart.js with streaming updates:
import { Chart } from 'react-chartjs-2'
function CandlestickChart({ symbol }: { symbol: string }) {
const [data, setData] = useState<ChartData>({ labels: [], datasets: [] })
useEffect(() => {
const unsubscribe = subscribeToCandles(symbol, (candle) => {
setData((prev) => ({
labels: [...prev.labels, candle.timestamp],
datasets: [{
...prev.datasets[0],
data: [...prev.datasets[0].data, {
x: candle.timestamp,
o: candle.open,
h: candle.high,
l: candle.low,
c: candle.close,
}]
}]
}))
})
return unsubscribe
}, [symbol])
return <Chart type="candlestick" data={data} />
}Optimistic UI for Trade Execution
Users expect instant feedback when placing orders. Update UI optimistically, then reconcile:
async function executeTrade(order: Order) {
const tempId = generateTempId()
// 1. Optimistically add order to UI
addOrder({ ...order, id: tempId, status: 'pending' })
try {
// 2. Send to backend
const confirmedOrder = await api.placeOrder(order)
// 3. Replace temp order with confirmed
updateOrder(tempId, confirmedOrder)
} catch (error) {
// 4. Rollback on error
removeOrder(tempId)
showError('Order failed: ' + error.message)
}
}Error Boundaries for Resilience
A single component crash shouldn't kill the entire dashboard:
class ChartErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError(error: Error) {
return { hasError: true }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
logErrorToService(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <div>Chart failed to load. <button onClick={() => window.location.reload()}>Reload</button></div>
}
return this.props.children
}
}Performance Metrics: Before and After
Optimization results from production:
- ✅ Frame rate: 25fps → 60fps during high volatility
- ✅ Time to interactive: 3.2s → 1.1s
- ✅ Memory usage: 180MB → 85MB (sustained)
- ✅ WebSocket reconnect time: 12s → 2s average
Lessons Learned
Key takeaways from building at scale:
- Measure first: Use React DevTools Profiler before optimizing
- Web Workers are underused: They solve most performance bottlenecks
- State libraries matter: Zustand's fine-grained subscriptions beat Redux for this use case
- Optimistic UI is mandatory: Users notice 100ms delays in trading UIs
Conclusion
Real-time dashboards demand respect for performance budgets. Every component, every render, every state update must be intentional. The patterns shown here—Web Workers, selective subscriptions, virtualization, and optimistic updates—aren't premature optimization. They're the baseline for professional real-time UIs.
Start with these patterns from day one. Retrofitting performance into a slow dashboard is 10x harder than building it right from the start.

Written by M. Alaa Hedhly
Full-Stack Developer | ML Engineer | DevOps Engineer
I build modern web applications, ML systems, and trading automation. I also manage production infrastructure with Docker, Coolify, and Traefik.