Back to React
React
medium
mid

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.

8 min read·~5 min to think through

Dashboard with independent concerns = three orthogonal data flows. Don't conflate them.

The shape

tsx
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

tsx
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:

tsx
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.

tsx
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

tsx
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

tsx
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

ConcernTool
Auth, theme, locale (rare updates, many consumers)Context
Server state (caching, refetch, sync)React Query
Client state with frequent updates + selectorsZustand / Jotai
Cross-cutting events (notifications)EventSource + Zustand
Mutations with optimistic UIReact 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.

Senior engineer discussion

Senior framing: dashboards are coordination problems. The right answer isn't 'use library X' but 'each concern gets its own subscriber model'. Polling is fine for slow data; WebSocket for real-time; SSE for one-way streams; React Query for caching. Compose, don't homogenize.

Related questions