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)
- Inject fetcher — testability
- AbortController per tick
- visibilitychange to pause
- MVP — one metric card + interval; then pause + stale
- 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
- Overlapping fetches without abort → UI flicker / reordering
- setState after unmount
- Polling while tab backgrounded for hours
- Replacing UI with spinner on every poll — keep stale-while-revalidate UX
- Unstable fetcher identity restarting effect every render — wrap in
useCallbackor 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.
Related on this site
- Realtime WebSocket UI Design
- Design a Dashboard Analytics UI
- Error Monitoring and RUM Design
- Machine Coding Interview Framework