100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
React 19 & Ecosystem
50 minintermediate

Hooks Practice: Build a Real-Time Dashboard

What You'll Build

You will build a real-time metrics dashboard that exercises every hook from this module: useReducer to manage dashboard state, useEffect to subscribe to a live data feed with proper cleanup, useRef to track values across renders without re-rendering, useMemo to derive aggregates from incoming data, and a custom hook to encapsulate the live-data subscription. It is a realistic feature that ties the module together.

The dashboard receives a stream of metric updates (simulated with an interval), keeps a rolling window of recent readings in reducer state, computes aggregates like averages and peaks with useMemo, and cleans up its subscription on unmount. You will extract the subscription into a reusable custom hook, leaving the component clean and declarative.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a team first drills its fundamental skills — a clean cover drive, a tidy pickup-and-throw, a reliable catch — before combining them into match play, you first build clean, reusable components before composing them into features. The insight is that mastering the fundamentals in isolation makes the combined performance solid: a team with grooved basics plays fluent cricket, exactly as an app of well-built components composes into a fluent UI.

Prerequisites

  • Completion of lessons 06–09, or equivalent familiarity with useState/useReducer, useEffect, useRef/useMemo, and custom hooks.
  • A React 19 + Vite project running locally (npm run dev).
  • Comfort with reducers, effect cleanup, and the rules of hooks.
  • Basic familiarity with setInterval/clearInterval for simulating a live feed.
  • Understanding that custom hooks share logic, not state.

Setup & Project Structure

In your React project, create a custom hook useLiveMetrics that simulates a live data feed (an interval emitting random readings) and a Dashboard component that consumes it. You will manage the rolling window of readings with useReducer, derive aggregates with useMemo, and track the latest reading with useRef.

Analogy🏏Cricket
🏏 Think of it like cricket: before a tour you set up the training camp and give each specialist their own net — one lane for openers, one for the spinner, one for the death bowler — so each rehearses a focused, self-contained skill before you bring them together for a full practice match. Just as you scaffold a React project with Vite and a components folder holding Button, Input, Card, Toggle, and Modal as separate files, a camp organises separate stations for each role, each drilling one prop-driven job. Just as each component stays focused and driven by the props passed in, each net drill runs to a clear brief — 'defend the yorker', 'rotate strike' — nothing overloaded. And just as you finally compose them in App to see them work together, the camp ends with a full simulation where every rehearsed piece slots into one XI. The payoff: assembling a working whole is easy because every reusable part was built and tested in isolation first.

Structure the code so the subscription logic lives entirely in the custom hook, and the Dashboard component only declares state shape, dispatches updates, and renders. This separation is the core lesson: the component stays readable while the hook owns the live-data mechanics and cleanup.

bash
// src/hooks/useLiveMetrics.js  — custom hook owning the live feed + cleanup
import { useEffect } from "react";
export function useLiveMetrics(onReading, intervalMs = 1000) {
  useEffect(() => {
    const id = setInterval(() => {
      onReading({ value: Math.round(Math.random() * 100), at: Date.now() });
    }, intervalMs);
    return () => clearInterval(id);     // cleanup: stop the feed on unmount
  }, [onReading, intervalMs]);
}

// src/Dashboard.jsx — consumes the hook; manages state with useReducer
// (built across the steps below)

Step 1 — Model Dashboard State with useReducer

Define a reducer that maintains a rolling window of the last N readings. Actions include adding a reading (append and trim to N) and clearing. This centralises the state-transition logic — appending, trimming the window, resetting — in one pure, testable function rather than scattered setters.

Initialise the reducer with an empty readings array and a window size. The component dispatches an 'add' action for each incoming reading; the reducer immutably builds the new array and trims it. Using a reducer here pays off because each update is a coordinated transition (append plus trim), exactly the case useReducer suits.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a scoreboard keeps only the last several overs visible, dropping the oldest as each new over arrives — a rolling window maintained by one consistent rule — the reducer keeps the last N readings, trimming the oldest on each addition. The insight is that a fixed-size recent view needs one clear rule applied on every update, exactly what a reducer provides for the rolling window.
jsx
import { useReducer } from "react";
const WINDOW = 20;

function metricsReducer(state, action) {
  switch (action.type) {
    case "add":
      return { ...state,
        readings: [...state.readings, action.reading].slice(-WINDOW) };  // trim window
    case "clear":
      return { ...state, readings: [] };
    default:
      return state;
  }
}
// in component: const [state, dispatch] = useReducer(metricsReducer, { readings: [] });

Step 2 — Wire the Custom Hook and useRef

Consume useLiveMetrics in the Dashboard, passing a stable callback (wrapped in useCallback) that dispatches an 'add' action for each reading. Use useRef to track the latest reading and a count of total readings received — values you want to persist across renders without triggering extra re-renders.

The ref demonstrates the distinction from state: the total-readings counter increments on every feed tick but does not itself need to re-render the UI on each change, so a ref is appropriate for an internal tally, while the rolling window (which is displayed) lives in reducer state. Pass a stable callback so the effect inside the hook does not re-subscribe each render.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as the scorer displays the live over-by-over chart (state, shown) while privately keeping a running count of total deliveries for their own reference (a ref, not displayed), the Dashboard renders the window from state while tracking internal tallies in refs. The insight is that some figures are for the audience and some are private bookkeeping — the visible chart versus the scorer's private count — mapping exactly to state versus ref.
jsx
import { useCallback, useRef } from "react";

function Dashboard() {
  const [state, dispatch] = useReducer(metricsReducer, { readings: [] });
  const totalCount = useRef(0);                    // persists, no re-render
  const lastReading = useRef(null);

  const handleReading = useCallback((reading) => {  // stable identity for the hook's effect
    totalCount.current += 1;                         // private tally (ref)
    lastReading.current = reading;
    dispatch({ type: "add", reading });             // displayed window (state)
  }, []);

  useLiveMetrics(handleReading, 1000);               // custom hook owns subscription+cleanup
  // ... render below
}

Step 3 — Derive Aggregates with useMemo

From the rolling window of readings, compute aggregates — average, minimum, maximum, and a simple trend — with useMemo so they recompute only when the readings change, not on every unrelated render. These are derived values, correctly computed from state rather than stored as additional state, exactly as the effects lesson advised.

Render the dashboard: the live aggregates, the current window of readings, and the private ref tallies where useful. Because the aggregates are memoised over the readings array, they recompute precisely when new data arrives and are skipped otherwise — a genuine use of useMemo where the derivation runs frequently over a changing list.

Analogy🏏Cricket
🏏 Think of it like cricket: Just as a broadcast computes run rate, projected total, and required rate from the ball-by-ball data — derived figures recalculated as new balls are bowled — the dashboard computes average, min, max, and trend from the readings window, recalculated as readings arrive. The insight is that the headline figures are derived from the raw feed on each update, not stored separately — computed fresh from the data, exactly as useMemo derives them from the readings.
jsx
import { useMemo } from "react";

// inside Dashboard, after the hooks above:
const stats = useMemo(() => {
  const r = state.readings;
  if (r.length === 0) return { avg: 0, min: 0, max: 0 };
  const values = r.map(x => x.value);
  return {
    avg: Math.round(values.reduce((a, b) => a + b, 0) / values.length),
    min: Math.min(...values),
    max: Math.max(...values),
  };
}, [state.readings]);   // recompute only when readings change

return (
  <section>
    <h2>Live Metrics</h2>
    <p>Avg {stats.avg} · Min {stats.min} · Max {stats.max}</p>
    <p>Total received: {totalCount.current} · Window: {state.readings.length}</p>
    <button onClick={() => dispatch({ type: "clear" })}>Clear</button>
  </section>
);

Step 4 — Testing & Verification

Run the dashboard and confirm the full loop: readings arrive every second, the rolling window stays capped at the window size, the aggregates update as data flows, and the private ref tally counts every reading. Then unmount the Dashboard (e.g. toggle it off) and confirm via console logging that the interval is cleared — proving the custom hook's cleanup works and no feed leaks.

Analogy🏏Cricket
🏏 Think of it like cricket: the final selection trial where you field the full XI and check every player does their exact job under match conditions. Just as you compose all five components in App and verify each behaviour, a captain runs a scenario and confirms each role fires correctly: the buttons render variants and fire onClick like bowlers delivering their set variations on cue; the controlled Input updating parent state on every keystroke is the batter feeding the scorer every single run in real time; the Toggle flipping and reporting its state is the third-umpire light switching out and back and signalling the result. Just as the Modal opens, closes on the backdrop or button but not when its own content is clicked, a DRS review triggers on a genuine appeal, resolves cleanly, but isn't set off by incidental noise near the stumps. And just as stateless components re-render purely from props while stateful ones manage their own, pure specialists execute exactly the brief while others track their own tally. The payoff: verified, trustworthy behaviour before the real match.
jsx
// Toggle the dashboard to verify cleanup on unmount
function App() {
  const [show, setShow] = useState(true);
  return (
    <>
      <button onClick={() => setShow(s => !s)}>{show ? "Hide" : "Show"}</button>
      {show && <Dashboard />}
    </>
  );
}
// Add a console.log("feed stopped") in the hook's cleanup (return () => {...})
// and confirm it logs when you hide the Dashboard — no orphaned interval.

Warning: If you pass an inline arrow function to useLiveMetrics instead of a stable useCallback'd one, the effect's dependency changes every render, so it clears and recreates the interval on every render — causing missed ticks or runaway timers. Stabilise the callback with useCallback (empty deps here, since it only uses refs and dispatch, which are stable), so the subscription is set up once and cleaned up once.

Extension Challenge: Replace the simulated interval with a real WebSocket in useLiveMetrics, keeping the same cleanup contract (close the socket on unmount). Then add a useDebounce custom hook to throttle a search/filter input over the readings, and a useLocalStorage hook to persist the window size across reloads — composing several custom hooks in one dashboard to see how cleanly logic separates from rendering.

  • useReducer manages the rolling-window state with one pure transition (append + trim).
  • A custom hook (useLiveMetrics) encapsulates the subscription and its cleanup, keeping the component clean.
  • useRef tracks private tallies (total count, last reading) that persist without triggering re-renders.
  • useMemo derives aggregates from the readings, recomputing only when readings change.
  • Pass a stable (useCallback'd) callback to the hook so the effect doesn't re-subscribe every render.
  • Effect cleanup stops the feed on unmount — verify no orphaned interval/socket leaks.
Lesson 10 of 35
0% complete