EDUCATIONOverviewStudentsTeachersSchoolsCurriculumProject Library
GUIDED PROJECT·Advanced·~3 weeks·15 min read

Build a Trading Dashboard

Build a live trading dashboard — real-time price feeds streaming into charts that update as the market moves. You'll consume a high-frequency WebSocket firehose without drowning the UI, keep a performant chart in sync, and design a data-dense interface that stays readable.

WHAT YOU'LL WALK AWAY WITH
  • A dashboard streaming live market data into charts that update in real time
  • Consuming a high-frequency WebSocket feed without overwhelming the browser
  • Performant charting — updating a moving series without janky re-renders
  • A data-dense layout that stays readable: the real challenge of financial UIs
TIER
Advanced
TIME
~3 weeks
SKILLS
Data pipelines, charts, live feeds
STACK
React, WebSockets, charting
PORTFOLIO
High — data-heavy frontend engineering
REQUIRES
Expense Tracker or equivalent

Most frontend work is calm: you fetch some data, you render it, the user reads it. A trading dashboard breaks that calm on purpose. Prices don't arrive when you ask — they arrive in a torrent, dozens of updates a second, and your job is to show them without turning the browser into a slideshow. That constraint is what makes this the project where you stop thinking about what to render and start thinking about when and how often. It's data-heavy frontend engineering distilled to its hardest, most useful lesson.

This assumes you've built a full-stack app before — the Expense Tracker or equivalent — so React state, useEffect, and TypeScript feel familiar. You do not need any finance background; the market is just a convenient source of fast, real data. Honest scope note: this build deliberately does less than a real trading terminal. No orders, no portfolio, no auth. One symbol, one live chart, done right — because the performance lesson is the whole point, and piling on features only obscures it.

Info

Estimated time: ~3 weeks, part-time. Not because there's a lot of code — there isn't — but because the core idea (getting data out of React's render path) is genuinely new and takes real reps to internalize. You'll write a naive version that janks, feel exactly why, and then fix it. That arc is the learning; skipping to the fix teaches you nothing.

Note

This is an educational engineering project. It is not financial or investment advice, and nothing you build here should be used to make real trading decisions. The market feed is just a firehose of live data that happens to be free and fast — a perfect teaching tool, nothing more.

What you're building

A single-page dashboard for one trading pair (say, BTC/USDT). At the top, a live price with its recent change. Below it, a candlestick or line chart that extends in real time as new prices arrive. Around it, a few dense-but-readable stats — last price, 24h change, connection status. It reads from a public market feed and updates continuously, smoothly, for as long as you leave it open.

Stack, and why each piece earns its place:

  • React — for the UI, but with a twist you'll learn: most of the live data will live outside React state, and that's the whole trick.
  • WebSockets — the feed pushes data to you over a persistent connection; you don't poll for it. Request/response can't do real-time, so you need a channel that stays open.
  • TradingView lightweight-charts — a charting library built specifically for high-volume financial series. It has an imperative update() API that lets you feed it new data without going through React — exactly what a hot data path needs.

The data source: a free public market-data WebSocket. This guide uses Binance's public streams, which need no API key and no account for market data. Like the keyless API in the Weather App, that keeps you unblocked and keeps secrets out of browser code.

Milestone 0 — Design: the data flow from feed to chart

Before installing anything, draw the pipe on paper. Data travels: WebSocket → a buffer → a flush on a timer → the chart. Sketch it and label what each stage is responsible for, because this diagram is the entire architecture:

[Binance WS]  →  onmessage (fires ~10-50x/sec)
                     │  push tick into a ref (NOT state)
                     ▼
              [ buffer: useRef([]) ]   ← the hot path, zero re-renders
                     │
                     │  requestAnimationFrame / throttle (~60x/sec max)
                     ▼
              flush: chart.update(latest)   ← imperative, bypasses React
                     │
                     ▼
              setState only for the *display* number (throttled)

The single most important decision in this whole project is visible right here: the incoming messages do not call setState. They land in a useRef buffer. Something slower and steadier — a timer or an animation frame — drains that buffer into the chart. React's render loop and the network's message loop run at different speeds, and this diagram is how you stop the fast one from driving the slow one into the ground.

Heads up

The trap that sinks this project: calling setState inside the WebSocket's onmessage. It looks right — data arrives, you set state, the UI updates. But at 30 messages a second, that's 30 re-renders a second of your whole component tree, and the browser can't keep up. The page stutters, input lags, the fan spins. The messages arriving and the screen updating must be decoupled. If you take one thing from this build, take that. We'll build the naive version first so you feel it — then fix it in Milestone 4.

Milestone 1 — Connect to a live market feed over WebSocket

Get bytes flowing before you draw anything. Scaffold a React app and open a connection:

npm create vite@latest trading-dashboard -- --template react-ts
cd trading-dashboard
npm install lightweight-charts
// A single symbol's trade stream. No key, no account needed.
const url = "wss://stream.binance.com:9443/ws/btcusdt@trade";
const ws = new WebSocket(url);

ws.onopen = () => console.log("connected");
ws.onmessage = (event) => {
  const trade = JSON.parse(event.data);
  // trade.p = price (string), trade.T = trade time (ms)
  console.log(trade.p);
};
ws.onclose = () => console.log("closed");
ws.onerror = (err) => console.error(err);

Run it and watch the console fill. Read the real messages before you build anything on them — the same habit the Weather App drilled. You'll see a flood of trade objects; note that trade.p is a string, not a number, and that the messages come far faster than you'd want to render.

What to understand before moving on:

  • A WebSocket is a persistent, two-way pipe — unlike fetch, which is one question and one answer, this connection stays open and the server pushes to you. That's the only way to do real-time.
  • The server sets the pace, not you. You can't slow the feed down by asking nicely; the messages arrive when trades happen. Controlling that rate is your problem, downstream — which is exactly Milestones 2 and 4.
  • onclose and onerror are not optional. Connections drop — networks hiccup, servers restart, laptops sleep. A dashboard that dies on the first disconnect isn't a dashboard.
Heads up

Two things bite everyone here. First, reconnection: when onclose fires, you need to reconnect — but not in a tight loop that hammers the server the instant it's down. Use exponential backoff (wait 1s, then 2s, then 4s…) so a struggling server gets room to recover. Second, backpressure: if your tab is backgrounded or the CPU is pinned, messages queue up faster than you process them. Your buffer (next milestone) is what absorbs that — but know that an unbounded buffer is a memory leak waiting to happen. Cap it.

Milestone 2 — Taming the firehose: throttling and buffering high-frequency updates

You have data arriving too fast. The fix is a two-part pattern you'll reuse for the rest of your career: buffer the fast thing, drain it on a slow interval. Put incoming trades into a ref-held array, and separately, on a timer, take whatever accumulated and process just the latest:

import { useRef, useEffect } from "react";

function useTradeBuffer(onFlush: (latestPrice: number) => void) {
  const buffer = useRef<number[]>([]);

  useEffect(() => {
    const ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@trade");
    ws.onmessage = (event) => {
      const trade = JSON.parse(event.data);
      buffer.current.push(Number(trade.p)); // hot path: just push, never render
      if (buffer.current.length > 500) buffer.current.shift(); // cap it (backpressure)
    };

    // Drain the buffer at a steady, sane rate — not per-message.
    const timer = setInterval(() => {
      if (buffer.current.length === 0) return;
      const latest = buffer.current[buffer.current.length - 1];
      buffer.current.length = 0; // clear what we consumed
      onFlush(latest);
    }, 100); // 10x/sec is plenty for a human eye

    return () => { ws.close(); clearInterval(timer); };
  }, [onFlush]);
}

What to understand, because this is the mechanical heart of the whole project:

  • The onmessage handler does the least work possible — parse, push, cap. No rendering, no chart calls, no state. It runs dozens of times a second, so it must be cheap.
  • The interval decides the display rate. The market can trade 50 times a second; your eyes can't tell 50 from 10. So you flush at a human-friendly cadence and simply drop the in-between ticks. Deliberately throwing data away is not a bug here — it's the design.
  • The cap (> 500) is your backpressure valve. If the flush ever falls behind, the buffer can't grow without bound and eat all your memory. You keep the recent tail and discard the rest.

Milestone 3 — The chart: rendering a live price series

Now draw something. lightweight-charts is imperative — you create a chart, add a series, and call methods on it. That's a feature, not a quirk: it means you can update the chart without a React render. Set it up once and hold references:

import { useEffect, useRef } from "react";
import { createChart, type IChartApi, type ISeriesApi } from "lightweight-charts";

export function PriceChart() {
  const containerRef = useRef<HTMLDivElement>(null);
  const seriesRef = useRef<ISeriesApi<"Line"> | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;
    const chart: IChartApi = createChart(containerRef.current, {
      height: 400,
      layout: { background: { color: "#0b0e11" }, textColor: "#d1d4dc" },
    });
    seriesRef.current = chart.addLineSeries({ color: "#26a69a" });
    return () => chart.remove();
  }, []);

  return <div ref={containerRef} />;
}

What to understand: the chart lives in a useRef, created once in an empty-dependency useEffect, and torn down on unmount. It is not React state and it does not re-render when prices change. React's only job was to mount the container <div> and hand it to the charting library. From here on, new prices reach the chart through seriesRef.current, not through props or state. Hold that separation in your head — it's what Milestone 4 exploits.

Milestone 4 — Real-time updates without jank (batching, refs, requestAnimationFrame)

This is the milestone the whole project exists for. Wire the buffer to the chart — but push updates through the imperative chart API on an animation frame, so the browser draws exactly as often as it can and never more:

import { useRef, useEffect, useCallback } from "react";

// Inside PriceChart, alongside the chart setup:
const pending = useRef<{ time: number; value: number } | null>(null);
const rafId = useRef<number>(0);

const scheduleDraw = useCallback(() => {
  if (rafId.current) return; // a frame is already queued — coalesce
  rafId.current = requestAnimationFrame(() => {
    rafId.current = 0;
    if (pending.current && seriesRef.current) {
      seriesRef.current.update(pending.current); // imperative: no React render
      pending.current = null;
    }
  });
}, []);

// The flush handler from Milestone 2 calls this instead of setState:
const onFlush = useCallback((price: number) => {
  pending.current = { time: Math.floor(Date.now() / 1000), value: price };
  scheduleDraw();
}, [scheduleDraw]);

What to understand — read this slowly, it's the payoff of the entire build:

  • requestAnimationFrame aligns your work to the browser's paint cycle (~60fps). Even if ten flushes land before the next frame, scheduleDraw coalesces them into one update(). You never draw faster than the screen refreshes, which is the definition of "no wasted work."
  • seriesRef.current.update(...) bypasses React entirely. The price changes on screen and React's render function never ran. That's not a hack — it's the correct tool for a value that changes 30 times a second.
  • The display number is separate. For the big price readout in the corner, you do use setState — but throttled to maybe twice a second, because that's a React-shaped, human-paced piece of UI. The chart's hot path and the readout's slow path are different problems with different tools. Knowing which is which is the skill.
Tip

Do the experiment. First wire onFlush to a plain setState that stores every price and re-renders — watch the CPU climb and the page stutter under a busy market. Then switch to the ref-plus-rAF version above and watch it go glassy-smooth. Feeling that difference in your own hands is worth more than any explanation. Commit right after — this is the natural checkpoint to run Cartara against your repo (see the last section), because this is exactly where "it works" and "I understand why" split.

Milestone 5 — The dashboard layout: dense but readable

A trading UI shows a lot at once without feeling cluttered — that tension is the real design challenge of financial interfaces. Arrange the chart, a price header, and a small stats strip with CSS grid, and lean on hierarchy so the eye knows where to land:

<main className="dashboard">
  <header className="ticker">
    <span className="symbol">BTC/USDT</span>
    <span className="price">{displayPrice}</span>
    <span className={change >= 0 ? "up" : "down"}>{change.toFixed(2)}%</span>
    <span className="status">{connected ? "● live" : "○ reconnecting"}</span>
  </header>
  <PriceChart />
</main>

What to understand: density isn't about cramming — it's about hierarchy. One thing is biggest (the price). Color carries meaning consistently (green up, red down, everywhere). Secondary data is present but visually quieter. And notice displayPrice and connected here are React state — because a header that updates a couple times a second is exactly what React is good at. You're using state where it fits and refs where it doesn't. That judgment, not any single API, is what this project is teaching.

Milestone 6 — Ship it

Because the dashboard is a static SPA reading a public feed, deployment is nearly free — there's no server of your own to run:

  1. Push the repo to GitHub.
  2. Import it into Netlify or Vercel; build with npm run build, publish the dist folder. Both give you a public URL in minutes.
  3. Open the live link, leave it running, and watch it track the market in real time.

Then stress it honestly: open dev tools, watch the CPU and frame rate while the market is busy, and confirm it stays smooth. That performance check is the whole point of the project, made visible — a dashboard that stays at 60fps under a live firehose is the thing you built.

Stuck? Study a production build

When something won't click, real code beats another tutorial — after you've built your own version, so you can see what they did differently and why, not just copy an answer:

  • tradingview/lightweight-charts — the charting library this build uses. Its examples show the imperative update() patterns for high-volume series you wired by hand, plus candlesticks and time-scale handling you can grow into.
  • binance/binance-spot-api-docs — the market-data feed's own docs. When a stream isn't behaving as you expect — message shape, stream names, rate limits — the answer is here.

The order matters: build first, read second. Reading a finished implementation before you've fought the jank yourself teaches you what the code is, not why real-time UIs are shaped that way.

Build it with Cartara

Here's the honest part. You could get this dashboard working by pasting each block above and never grasping why the prices go through a ref instead of state, or what requestAnimationFrame is actually buying you. It would even run smoothly. But you'd be unable to apply the one lesson that made it fast to the next live UI you build — and increasingly, an AI assistant will have written that hot path for you, which makes the gap between "it's smooth" and "I know why it's smooth" completely invisible until a harder problem exposes it.

That gap is exactly what Cartara exists to close. As you build:

  • Point Cartara at your repo. It watches the code that lands — including anything an AI assistant generated — and turns each meaningful change into a checkpoint for understanding, not just a commit.
  • Explain your changes back. When you add the buffer, the rAF batching, or the reconnection logic, Cartara prompts you on the reasoning and trade-offs — the same questions a reviewer or interviewer would ask about a performance-critical path.
  • See your comprehension, not just your output. You get an honest signal on whether you understand why the data stays out of React's render — the single idea this project is built to teach — so you can shore it up before it shows up in a review.

That's the loop this whole library is built around: build something real, then prove to yourself you understand it. The dashboard is the project. The understanding is the point.

Turn a live dashboard into understanding you can prove

Point Cartara at your trading-dashboard repo and it captures what you actually learned about keeping a UI fast under load — the honest way to make a performance-critical, AI-assisted build truly yours.

Join the waitlist

Reference implementations

Production code worth studying — afteryou've built your own version.

tradingview/lightweight-charts
TradingView's open-source financial charting library — built for exactly the real-time, high-volume series this project renders.
binance/binance-spot-api-docs
Docs for a real, free market-data WebSocket feed you can stream live prices from — the data source for this build.
← All projects

Build it with Cartara

Point Cartara at your repo as you build this project. It captures what you actually learned from every change — including the code an AI assistant wrote — so an AI-assisted build becomes understanding you can prove.

Join the waitlist