For a dashboard where chart updates, notifications, and data fetches happen independently, how would you use React Context and custom hooks?
Treat each concern as an independent slice with its own data source and hook. Charts: useChartData (React Query, polling or WebSocket). Notifications: useNotifications (subscribed via Context or external store). Data fetches: useQuery per resource. Compose at the page level. Context only for cross-cutting state (theme, auth). External libs (Zustand) when state must update at high frequency or across many consumers without re-render storms.
Dashboard with independent concerns = three orthogonal data flows. Don't conflate them.
The shape
function Dashboard() {
return (
<Layout>
<Header><NotificationBell /></Header>
<Sidebar />
<main>
<ChartGrid />
<DataTable />
</main>
</Layout>
);
}Each child reads its own data — no monolithic top-level state.
1. Charts: useQuery + polling/WS
function useChartData(chartId: string) {
return useQuery({
queryKey: ['chart', chartId],
queryFn: () => fetch(`/api/chart/${chartId}`).then(r => r.json()),
refetchInterval: 30_000, // poll every 30s
refetchOnWindowFocus: true,
});
}
function ChartTile({ id }: { id: string }) {
const { data, isLoading } = useChartData(id);
if (isLoading) return <Skeleton />;
return <BarChart data={data} />;
}For lower-latency, swap polling for WebSocket:
useEffect(() => {
const sock = new WebSocket('/chart-updates');
sock.onmessage = (e) => {
const { chartId, data } = JSON.parse(e.data);
queryClient.setQueryData(['chart', chartId], data);
};
return () => sock.close();
}, []);2. Notifications: dedicated hook + store
Notifications update at irregular intervals from a server-sent stream. Multiple components read them (bell badge, banner, log panel). Use a tiny store (Zustand) or Context with a stable dispatch.
import { create } from 'zustand';
const useNotifs = create<Notif[]>(set => ({
items: [],
add: (n) => set(s => ({ items: [n, ...s.items] })),
remove: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })),
}));
// init once
useEffect(() => {
const es = new EventSource('/api/notifications/stream');
es.onmessage = e => useNotifs.getState().add(JSON.parse(e.data));
return () => es.close();
}, []);
// consumer
function Bell() {
const count = useNotifs(s => s.items.length); // selector — re-renders only on count change
return <BellIcon count={count} />;
}Zustand selectors are key — useNotifs(s => s.items.length) re-renders only when count changes, even if other notifications fields update.
3. Data fetches: useQuery per resource
function DataTable() {
const { data } = useQuery({
queryKey: ['orders', { status: 'pending' }],
queryFn: () => fetch('/api/orders?status=pending').then(r => r.json()),
});
return <Table rows={data ?? []} />;
}Why three independent flows
- Chart polling shouldn't trigger notification re-render.
- Notification arrival shouldn't refetch tables.
- Table filter change shouldn't disturb charts.
If you put all three in one Context provider, every update re-renders every consumer.
Custom hooks abstract the source
function useDashboardKpi() {
// could come from polling, WS, RSC, anywhere — caller doesn't care
return useQuery({ queryKey: ['kpi'], queryFn: fetchKpi });
}Swap implementation later without touching consumers.
Context vs Zustand vs Query
| Concern | Tool |
|---|---|
| Auth, theme, locale (rare updates, many consumers) | Context |
| Server state (caching, refetch, sync) | React Query |
| Client state with frequent updates + selectors | Zustand / Jotai |
| Cross-cutting events (notifications) | EventSource + Zustand |
| Mutations with optimistic UI | React Query useMutation |
Re-render isolation
The dashboard's biggest perf risk is one component triggering rerenders across the whole tree. Avoid:
- Single mega-context with everything inside.
- A 'usePageState' hook that combines all data.
- Inline objects in Provider values.
Prefer:
- Separate contexts split by update frequency.
- Component-level useQuery — co-located fetches.
- Selectors via Zustand/Jotai/use-context-selector.
Senior framing
The right architecture follows the data — each concern picks its own update mechanism and subscriber model. Forcing all of it through one Context is what makes dashboards feel sluggish. The framework prompt is 'how do you keep concerns independent so updates don't cascade'.
Follow-up questions
- •When would you switch from polling to WebSockets?
- •Why use Zustand selectors over Context for notifications?
- •How do you avoid cascading re-renders when many concerns update independently?
Common mistakes
- •Single context with all dashboard state — every update re-renders everyone.
- •Polling everything at the same interval, regardless of need.
- •Storing server state in client state libs — duplicates and stales.
Performance considerations
- •Independent subscriptions keep re-render scope small. Polling every 30s for 5 charts is 10 requests/min — fine. Polling every 1s is 300/min — usually too much; switch to WS. Selectors prevent cascade re-renders even when state is large.
Edge cases
- •Tab not visible: pause polling to save battery (refetchOnWindowFocus).
- •WebSocket reconnect: queue missed updates and replay.
- •Notification spam: rate-limit on the client.
Real-world examples
- •Stripe Dashboard, Vercel Analytics, Linear inbox, Datadog monitoring. All independent panels with their own data sources, sharing only auth/theme via Context.