ESC

Type to search the knowledge base.

Polling Metrics Dashboard

Machine-coding brief for a metrics dashboard — polling, abort/stale safety, visibility pause, cards, and error UX.

intermediate4 min read
  • machine-coding
  • interview
  • react
  • async

Problem statement

Build a polling metrics dashboard: fetch metrics on an interval, render cards/charts placeholders, handle loading/error/stale data, and stop work when the tab is hidden. Interviewers score async lifecycle correctness more than D3 artistry.

Requirements

Must have

  • Fetch metrics from injected fetcher (mock OK)
  • Poll every intervalMs (e.g. 5s)
  • Display last successful payload (cards: value + label)
  • Loading on first fetch; keep previous data on refresh
  • Error state with retry
  • Cleanup interval + in-flight abort on unmount

Should have

  • Pause polling when document.visibilityState === 'hidden'
  • Show “Updated Xs ago”
  • Manual refresh button
  • Race-safe: ignore stale responses

Nice to have

  • Exponential backoff on errors
  • Sparkline component
  • Threshold coloring (warn/critical)

Planning (5 minutes out loud)

  1. Inject fetcher — testability
  2. AbortController per tick
  3. visibilitychange to pause
  4. MVP — one metric card + interval; then pause + stale
  5. Don’t setState after unmount

Architecture

MetricsDashboard
├── usePollingMetrics
├── MetricCard[]
├── LastUpdated
└── ErrorBanner

Types

type Metric = {
  id: string;
  label: string;
  value: number;
  unit?: string;
};

type MetricsResponse = {
  metrics: Metric[];
  serverTime: string;
};

type DashboardProps = {
  fetcher: (signal: AbortSignal) => Promise<MetricsResponse>;
  intervalMs?: number;
};

Implementation sketch

function usePollingMetrics(
  fetcher: DashboardProps["fetcher"],
  intervalMs = 5000
) {
  const [data, setData] = useState<MetricsResponse | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [updatedAt, setUpdatedAt] = useState<number | null>(null);
  const [visible, setVisible] = useState(
    () => document.visibilityState !== "hidden"
  );

  useEffect(() => {
    const onVis = () => setVisible(document.visibilityState !== "hidden");
    document.addEventListener("visibilitychange", onVis);
    return () => document.removeEventListener("visibilitychange", onVis);
  }, []);

  useEffect(() => {
    if (!visible) return;
    let cancelled = false;
    let timer: number | undefined;
    const controllerRef = { current: null as AbortController | null };

    async function tick() {
      controllerRef.current?.abort();
      const controller = new AbortController();
      controllerRef.current = controller;
      try {
        const res = await fetcher(controller.signal);
        if (cancelled) return;
        setData(res);
        setError(null);
        setUpdatedAt(Date.now());
      } catch (e) {
        if (cancelled) return;
        if (e instanceof DOMException && e.name === "AbortError") return;
        setError(e instanceof Error ? e.message : "Failed to load metrics");
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    tick();
    timer = window.setInterval(tick, intervalMs);

    return () => {
      cancelled = true;
      window.clearInterval(timer);
      controllerRef.current?.abort();
    };
  }, [fetcher, intervalMs, visible]);

  return { data, error, loading, updatedAt, refreshKey: visible };
}

UI

function MetricsDashboard({ fetcher, intervalMs = 5000 }: DashboardProps) {
  const { data, error, loading, updatedAt } = usePollingMetrics(
    fetcher,
    intervalMs
  );

  if (loading && !data) return <p role="status">Loading metrics…</p>;

  return (
    <div>
      <header>
        <h1>Metrics</h1>
        {updatedAt && (
          <p aria-live="polite">
            Updated {formatDistance(updatedAt)} ago
          </p>
        )}
      </header>
      {error && (
        <div role="alert">
          {error}{" "}
          <span>(showing last successful data if any)</span>
        </div>
      )}
      <ul className="cards">
        {data?.metrics.map((m) => (
          <li key={m.id}>
            <article>
              <h2>{m.label}</h2>
              <p>
                {m.value}
                {m.unit ?? ""}
              </p>
            </article>
          </li>
        ))}
      </ul>
    </div>
  );
}

function formatDistance(ts: number) {
  const s = Math.round((Date.now() - ts) / 1000);
  if (s < 60) return `${s}s`;
  return `${Math.floor(s / 60)}m`;
}

Accessibility essentials

  • Initial loading: role="status"
  • Errors: role="alert"
  • “Updated ago” polite live region (don’t spam every second — update on fetch)
  • Cards are articles with headings

Performance notes

  • Pause when hidden — saves battery and load
  • Abort previous tick if slow fetch overlaps interval
  • Prefer backoff when API is failing
  • Charts: don’t re-mount canvas every poll; update data prop

Footguns

  1. Overlapping fetches without abort → UI flicker / reordering
  2. setState after unmount
  3. Polling while tab backgrounded for hours
  4. Replacing UI with spinner on every poll — keep stale-while-revalidate UX
  5. Unstable fetcher identity restarting effect every render — wrap in useCallback or ref

Interview out-loud answer

Dashboard polling is an effect with setInterval plus AbortController, paused on document hidden. First load shows loading; later polls keep previous metrics and surface errors non-destructively. Fetcher is injected for mocks. I’d mention backoff and ETags as production upgrades, and WebSockets only if push latency is required.

Further reading