Dow Jones Real-Time Dashboards in React: A Production Architecture for Streaming Market Data and Technical Analysis
A master-class blueprint for rendering the Dow Jones under load: reconnect-hardened WebSocket ingestion, tear-free external stores, incremental Wilder indicators, and canvas rendering that holds 60fps during breaking-news tick bursts.
AI Generated with Qwen 3.0 via CometAPIThe Dow Jones Industrial Average is not a normal data feed. It is a price-weighted index derived from thirty constituent quotes and a divisor that changes whenever the index committee reconstitutes the average — and it arrives in bursty, latency-sensitive ticks that spike the instant Breaking News hits the wire. Wiring that into React naively produces a dashboard that tears, janks, and lies about staleness. This article builds the architecture that does not.
We will treat React strictly as a render layer, push transport and math out of the component tree, and make every technical analysis update O(1) instead of O(n).
Why the Dow Jones Is a Hostile Data Source for a React App
Three properties make DJIA dashboards harder than typical streaming UIs:
- It is a computed index, not a symbol. You cannot subscribe to
DJIand trust the number blindly; you must reconcile constituent-level quotes against the divisor, and understand when a price is a last trade versus a stale quote. - Traffic is bursty and non-uniform. Quiet periods produce a handful of prints per second. FOMC decisions, CPI prints, or a Breaking News headline produce thousands within a few hundred milliseconds.
- Every tick is a potential re-render. Thirty symbol components, a chart, a watchlist, and an indicator panel all subscribing to the same store is the fastest way to pin the main thread at 12fps.
The index math you must model correctly
The DJIA is a price-weighted average: the sum of the thirty constituent prices divided by a divisor that is adjusted for splits, spin-offs, and constituent changes so the index level stays continuous. If you display index level without tracking the divisor, your chart will show phantom gaps on reconstitution days.
export interface ConstituentQuote {
readonly symbol: string;
readonly last: number;
}
/**
* Price-weighted index level: sum(prices) / divisor.
* Quotes that are halted, unpriced, or non-finite are skipped rather than
* poisoning the whole average with NaN.
*/
export function priceWeightedLevel(
quotes: readonly ConstituentQuote[],
divisor: number,
): number | null {
if (!Number.isFinite(divisor) || divisor <= 0) {
throw new RangeError('divisor must be a positive finite number');
}
let sum = 0;
let counted = 0;
for (const quote of quotes) {
if (!Number.isFinite(quote.last) || quote.last <= 0) continue;
sum += quote.last;
counted += 1;
}
if (counted === 0) return null;
return sum / divisor;
}Returning null instead of a partial level forces the UI to render an explicit awaiting quotes state — which is precisely the honesty a trading surface needs.
Layered Architecture: Transport, Normalization, Store, Render
Enforce a hard boundary between four layers:
- Transport — a WebSocket client that owns reconnection, heartbeats, and frame validation. It knows nothing about React.
- Normalization — a decoder that converts wire frames into a strict
Ticktype, rejects malformed payloads, and appends to fixed-size ring buffers. - Store — an external, framework-agnostic state container that coalesces writes per animation frame and exposes primitive snapshots.
- Render — React components consuming snapshots via
useSyncExternalStore, plus a canvas layer for high-cardinality series.
The Lambda-style split between a streaming speed layer and a batch/historical layer matters here: your chart backfill comes from an HTTP request once, your live delta comes from the socket, and the two merge in the ring buffer, never in component state.
Building a Reconnect-Hardened WebSocket Ingestion Client
Network partitions are the normal case, not the exception. The client below implements exponential backoff with full jitter (to avoid a thundering herd of thousands of dashboards reconnecting in lockstep), application-level heartbeats, and defensive frame parsing.
export type Tick = {
readonly symbol: string;
readonly price: number;
readonly size: number;
readonly ts: number; // epoch milliseconds, UTC
};
export type FeedState = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
export interface FeedOptions {
readonly url: string;
readonly token: string;
readonly onTick: (tick: Tick) => void;
readonly onState?: (state: FeedState) => void;
readonly heartbeatMs?: number;
readonly baseBackoffMs?: number;
readonly maxBackoffMs?: number;
}
export class MarketFeed {
private socket: WebSocket | null = null;
private attempt = 0;
private heartbeat: ReturnType<typeof setInterval> | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private manuallyClosed = false;
constructor(private readonly opts: FeedOptions) {}
connect(): void {
if (this.socket?.readyState === WebSocket.OPEN) return;
this.manuallyClosed = false;
this.setState('connecting');
let socket: WebSocket;
try {
socket = new WebSocket(
`${this.opts.url}?token=${encodeURIComponent(this.opts.token)}`,
);
} catch {
// Wrong scheme or blocked by CSP: back off instead of throwing in render.
this.scheduleReconnect();
return;
}
this.socket = socket;
socket.onopen = () => {
this.attempt = 0;
this.setState('open');
this.startHeartbeat(socket);
};
socket.onmessage = (event: MessageEvent<string>) => {
try {
const parsed: unknown = JSON.parse(event.data);
if (isTick(parsed)) this.opts.onTick(parsed);
} catch {
// Malformed frame: drop it. Never let bad data reach the render loop.
}
};
socket.onerror = () => socket.close();
socket.onclose = () => {
this.stopHeartbeat();
if (this.manuallyClosed) {
this.setState('closed');
return;
}
this.scheduleReconnect();
};
}
private scheduleReconnect(): void {
const base = this.opts.baseBackoffMs ?? 500;
const max = this.opts.maxBackoffMs ?? 30_000;
const ceiling = Math.min(max, base * 2 ** this.attempt);
const delay = Math.random() * ceiling; // full jitter
this.attempt += 1;
this.setState('reconnecting');
this.reconnectTimer = setTimeout(() => this.connect(), delay);
}
private startHeartbeat(socket: WebSocket): void {
this.stopHeartbeat();
this.heartbeat = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'ping' }));
}
}, this.opts.heartbeatMs ?? 15_000);
}
private stopHeartbeat(): void {
if (this.heartbeat !== null) clearInterval(this.heartbeat);
this.heartbeat = null;
}
close(): void {
this.manuallyClosed = true;
if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);
this.stopHeartbeat();
this.socket?.close(1000, 'client shutdown');
this.socket = null;
}
private setState(state: FeedState): void {
this.opts.onState?.(state);
}
}
function isTick(value: unknown): value is Tick {
if (typeof value !== 'object' || value === null) return false;
const v = value as Record<string, unknown>;
return (
typeof v.symbol === 'string' && v.symbol.length > 0 &&
typeof v.price === 'number' && Number.isFinite(v.price) && v.price > 0 &&
typeof v.size === 'number' && Number.isFinite(v.size) &&
typeof v.ts === 'number' && Number.isFinite(v.ts)
);
}Bind connect in a useEffect and always close() in the cleanup — a leaked socket per HMR reload is the classic React dev-server memory leak.
Tear-Free State: useSyncExternalStore Without Re-Render Storms
The store must coalesce writes. Ticks arrive far faster than frames, so batch notifications into a single requestAnimationFrame flush and expose primitive snapshots.
type Listener = () => void;
class PriceStore {
private readonly prices = new Map<string, number>();
private readonly listeners = new Set<Listener>();
private flushHandle: number | null = null;
ingest = (tick: Tick): void => {
this.prices.set(tick.symbol, tick.price);
if (this.flushHandle === null) {
this.flushHandle = requestAnimationFrame(this.flush);
}
};
private flush = (): void => {
this.flushHandle = null;
for (const listener of this.listeners) listener();
};
subscribe = (listener: Listener): (() => void) => {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
};
/** Primitive snapshot: Object.is comparison keeps React loop-safe. */
getPrice = (symbol: string): number => this.prices.get(symbol) ?? Number.NaN;
}
export const priceStore = new PriceStore();
export function useLastPrice(symbol: string): number {
return useSyncExternalStore(
priceStore.subscribe,
() => priceStore.getPrice(symbol),
() => Number.NaN,
);
}Two rules are non-negotiable: getSnapshot must return a cached primitive or stable reference (returning a fresh object causes an infinite re-render loop), and the server snapshot must be deterministic to survive hydration. Coalescing at 60Hz caps notification volume at one flush per frame regardless of tick rate.
Incremental Technical Analysis: Never Recompute the Series
Recomputing a 14-period RSI over 5,000 bars on every tick is 70,000 operations per print — multiplied by thirty symbols, that is your dropped-frame budget gone. Implement indicators as stateful, O(1) updaters using Wilder's smoothing.
export class WilderRsi {
private prevClose: number | null = null;
private gainSum = 0;
private lossSum = 0;
private samples = 0;
private avgGain = 0;
private avgLoss = 0;
private value: number | null = null;
constructor(private readonly period = 14) {
if (!Number.isInteger(period) || period <= 0) {
throw new RangeError('period must be a positive integer');
}
}
update(close: number): number | null {
if (!Number.isFinite(close) || close <= 0) {
throw new TypeError('close must be a positive finite number');
}
if (this.prevClose === null) {
this.prevClose = close;
return null;
}
const change = close - this.prevClose;
const gain = Math.max(change, 0);
const loss = Math.max(-change, 0);
this.prevClose = close;
if (this.samples < this.period) {
this.gainSum += gain;
this.lossSum += loss;
this.samples += 1;
if (this.samples === this.period) {
this.avgGain = this.gainSum / this.period;
this.avgLoss = this.lossSum / this.period;
this.value = this.compute();
}
return this.value;
}
// Wilder smoothing: rma = (prev * (n - 1) + current) / n
this.avgGain = (this.avgGain * (this.period - 1) + gain) / this.period;
this.avgLoss = (this.avgLoss * (this.period - 1) + loss) / this.period;
this.value = this.compute();
return this.value;
}
private compute(): number {
if (this.avgLoss === 0) return 100;
const rs = this.avgGain / this.avgLoss;
return 100 - 100 / (1 + rs);
}
}For simple moving averages use a ring buffer that keeps a running sum, so each update is one subtraction, one addition, and a division:
export class SmaRing {
private readonly buf: Float64Array;
private idx = 0;
private filled = 0;
private sum = 0;
constructor(private readonly period: number) {
if (!Number.isInteger(period) || period <= 0) {
throw new RangeError('period must be a positive integer');
}
this.buf = new Float64Array(period);
}
update(value: number): number | null {
if (!Number.isFinite(value)) throw new TypeError('value must be finite');
const slot = this.idx % this.period;
this.sum += value - this.buf[slot];
this.buf[slot] = value;
this.idx += 1;
this.filled = Math.min(this.filled + 1, this.period);
return this.filled === this.period ? this.sum / this.period : null;
}
}Note the floating-point caveat: repeated add/subtract on Float64Array values accumulates drift against IEEE 754 semantics. For audit-grade P&L keep authoritative values in integer minor units and use floats only for display math.
Rendering at 60fps: Canvas Over React Nodes
Ten thousand JSX <circle> elements will lose. Draw the series on a canvas, scale by devicePixelRatio, and repaint inside a single animation frame:
export function useTimeSeriesCanvas(
draw: (ctx: CanvasRenderingContext2D, width: number, height: number) => void,
) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let frame = 0;
const render = () => {
frame = 0;
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, rect.width, rect.height);
draw(ctx, rect.width, rect.height);
};
const schedule = () => {
if (frame === 0) frame = requestAnimationFrame(render);
};
const observer = new ResizeObserver(schedule);
observer.observe(canvas);
schedule();
return () => {
observer.disconnect();
if (frame !== 0) cancelAnimationFrame(frame);
};
}, [draw]);
return canvasRef;
}Keep draw referentially stable (useCallback over refs, not state) or the effect tears down the observer on every tick.
Correctness Under Failure: Halts, Staleness, and Session Boundaries
A dashboard that shows a frozen price as live is worse than one that shows nothing. Track lastTickAt per symbol and render a stale badge past a threshold (typically 5 seconds during regular hours). Compute session state in exchange-local time, never the browser's:
const nyParts = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
weekday: 'short',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
export function isRegularSession(now: Date): boolean {
const parts = Object.fromEntries(
nyParts.formatToParts(now).map(({ type, value }) => [type, value]),
);
const weekday = parts.weekday ?? '';
if (weekday === 'Sat' || weekday === 'Sun') return false;
const minutes = Number(parts.hour) * 60 + Number(parts.minute);
return minutes >= 9 * 60 + 30 && minutes < 16 * 60; // 09:30–16:00 ET
}Use hourCycle: 'h23' rather than hour12: false — some engines emit hour 24 for midnight under the latter, which silently breaks interval math.
Operational checklist before you ship
- Reconnect uses exponential backoff with jitter and a hard ceiling.
- Heartbeat detects half-open sockets that never fire
onclose. - Store flushes are coalesced to one per animation frame.
- All indicator state resets explicitly on symbol change — never reuse a
WilderRsiacross tickers. - Staleness badge driven by
lastTickAt, not by socket state. - Chart Y-domain clamped with padding so a single Breaking News print does not rescale the whole view.
- Web Worker handles multi-symbol scans; transfer
Float64Arraybuffers instead of cloning. close()runs in effect cleanup and onpagehide.
Pitfalls to Avoid
- Returning a new object from `getSnapshot`. React compares with
Object.is; a fresh{ price, change }per call re-renders forever. Memoize in the store. - Subscribing every row to the whole store. Selectors keep a 30-symbol watchlist at 30 targeted updates per flush instead of 900.
- Recomputing indicators from full history. O(n) per tick is the single most common cause of trading UIs that stutter during breaking-news bursts.
- Trusting `Date.now()` for the X axis. Use the server tick timestamp; local clocks drift and Daylight Saving shifts will distort the session view.
- Autoscaling on every tick. Freeze the domain per pan/zoom gesture and decouple the axis animation from data arrival.
- Treating a halted constituent as a zero price. Skipping invalid quotes (as in
priceWeightedLevel) prevents a 300-point phantom drop in the index level.
Sources & Technical References
- React documentation — `useSyncExternalStore`, including the tearing guarantees and server snapshot contract.
- MDN Web Docs — WebSocket API and Page Visibility API.
- RFC 6455 — The WebSocket Protocol (IETF), for framing, ping/pong, and close codes.
- S&P Dow Jones Indices — Dow Jones Averages Methodology, defining the divisor and price-weighting rules.
- J. Welles Wilder Jr., New Concepts in Technical Trading Systems (1978) — original definition of RSI and Wilder smoothing (RMA).
- WHATWG HTML Standard — Web Workers and Structured clone / transferables.
- ECMA-402 — ECMAScript Internationalization API for
Intl.DateTimeFormattime zones andIntl.NumberFormatcurrency formatting. - IEEE 754-2019 — Standard for Floating-Point Arithmetic, the basis for the drift caveat in incremental indicator math.
- Nathan Marz & James Warren, Big Data: Principles and Best Practices of Scalable Realtime Data Systems (Manning, 2015) — speed/batch layer separation.
- MDN — `ResizeObserver` and Canvas API for DPR-aware rendering.
Discussion (0)
Continue Reading
More production-grade engineering deep dives and architectural guides

1B Spans/Day in Go: OTel SDK vs. eBPF vs. Hand-Rolled Tracing — Measured p99, CPU per Span, and Real Cost per 1M Spans
A billion spans a day sounds like a hyperscaler number — it is really just ~2,900 RPS. We benchmarked the OpenTelemetry Go SDK, eBPF auto-instrumentation, and a hand-rolled arena tracer on the same gRPC service to separate instrumentation CPU cost from the number that actually destroys observability budgets: storage and indexing per 1M spans.

Kubernetes Autoscaling at 10k Pods: Karpenter vs Cluster Autoscaler vs CAST AI, Measured
We ran identical HPA-driven load ramps from 2,000 to 10,000 pods across three EKS clusters to measure p99 scale-up latency, bin-packing waste, spot interruption recovery, and true cost per 1,000 pod-hours. Here is the methodology, the raw numbers, and the architectural reasons behind them.

Component Testing at 1M Runs/Month: Playwright CT vs Cypress CT vs Vitest Browser Mode — Measured p95, Flake Rate, and True CI Cost
We executed one million component test runs across Playwright CT, Cypress CT, and Vitest browser mode on identical hardware and measured p95 wall time, first-attempt flake rate, and throughput. The surprising result: CI compute is under 0.5% of your real bill — flake triage is the other 98%.