Back to React
React
hard
very high
senior

How would you prevent unnecessary re renders in a dashboard with live updates?

Slice state by widget, use selectors with referential stability, isolate live-update components behind their own subscriptions, and memoize where measurement justifies it.

9 min read·~15 min to think through

A live dashboard — trading desk, observability board, multiplayer presence — is the canonical re-render problem. Tens of websocket messages per second flow into the app; each one triggers a state update; if the whole tree subscribes to one big state blob, the whole tree re-renders, GC churns, dropped frames everywhere. The fix is not to chase memoization everywhere — it's to design the data flow so each update touches exactly the components that depend on the changed slice.

Strategies, ordered by impact:

  1. Slice the store by domain key. Don't keep { allTicks: Tick[] }. Keep { ticks: { [symbol]: Tick } } and let each widget subscribe to its key. With Zustand, that's a selector useStore(s => s.ticks[symbol]) — only re-renders when that one entry changes. Redux + useSelector does the same if you avoid creating new arrays/objects in the selector. Jotai atoms naturally give per-atom subscriptions. The win: a price update for AAPL re-renders only the AAPL card, not the 200 others.
  1. Stable selectors / memoized derivations. Selectors that return new arrays on every call (s => s.items.filter(...)) defeat reference equality and force every subscriber to re-render even when no logical change happened. Wrap them with reselect's createSelector, Zustand's useShallow, or your own memo. A correctly memoized derived selector is the single highest-leverage change in most dashboards.
  1. Push subscriptions to the leaves. The websocket handler should write into the store; subscribers pull. Don't lift the live state to a top-level provider and pass it down as props — that forces the whole tree through reconciliation. Each leaf decides which slice it cares about. This also makes the leaf a self-contained "live widget" you can test in isolation.
  1. Throttle / coalesce upstream. For high-frequency feeds, throttle into the store at 30–60Hz, not every WS message. Use requestAnimationFrame batching or a fixed-window aggregator. Render budget is the constraint; the user can't see 500 updates/sec anyway.
  1. React.memo for non-trivial leaves, paired with stable callbacks (useCallback) and stable derived data. Pure React.memo without stable refs in is worthless — props will still differ each render. Memoization is only useful when (a) the parent re-renders often, (b) the child render is non-trivial, and (c) most parent re-renders don't actually change the child's relevant props.
  1. useDeferredValue / startTransition for derived views (charts, aggregates) where it's OK to lag one frame behind the latest tick. The latest value still renders to the lightweight readout; the heavy chart catches up at lower priority.
  1. Virtualization. Long tables of streaming rows (react-window, @tanstack/react-virtual) — only the visible window mounts. Combined with row-level memoization, even 50k rows are cheap.
  1. Stable keys on list rows (key={row.id}, not array index). React's reconciler diffs by key — wrong keys destroy and re-mount every row on each update.
  1. useSyncExternalStore for external stores in concurrent React. It guarantees a consistent read across the tree (no tearing) and gives you the cheapest possible subscription path.
  1. Move work off the render thread. Heavy aggregation, decoding, or filtering for 10k+ rows belongs in a Web Worker. The main thread should be a thin renderer of pre-computed slices.

Anti-patterns to avoid:

  • Wrapping every component in React.memo "just in case." Comparison costs CPU; closure capture inflates memory; debugging gets harder. Profile first.
  • Putting the live state in React Context. Context broadcasts to every consumer on every change; it's the opposite of what you want for high-frequency updates.
  • Using indexes as keys. Use IDs.
  • Selectors that allocate objects/arrays inline.

Profiling workflow: use the React DevTools Profiler — record a few seconds of live updates, look at the flamegraph, and find components rendering with no commit-relevant prop change. Each one is either a missing memo, a missing selector slice, or a missing stable reference. Fix the worst three and the dashboard usually stops dropping frames.

Code

tsx
// store.ts (Zustand)
type Tick = { price: number; ts: number };
type State = { ticks: Record<string, Tick>; setTick: (sym: string, t: Tick) => void };
export const useTicks = create<State>((set) => ({
  ticks: {},
  setTick: (sym, t) => set((s) => ({ ticks: { ...s.ticks, [sym]: t } })),
}));

// Each card subscribes to *its* symbol only.
function Card({ symbol }: { symbol: string }) {
  const tick = useTicks((s) => s.ticks[symbol]);   // referentially stable per-symbol
  return <div>{symbol}: {tick?.price.toFixed(2)}</div>;
}
Per-symbol subscription so only the affected card re-renders

Follow-up questions

  • When does React.memo hurt performance?
  • How would you profile this in production?
  • Why is shallow equality often enough?

Common mistakes

  • Storing live data in `useState` at the page level — every tick re-renders the world.
  • Selectors that return new objects every call.
  • Using `useMemo` to 'fix' renders without verifying it changes anything.

Performance considerations

  • Memoization isn't free — comparison cost + retained closures.
  • 60 fps gives you a 16ms budget per frame; aim for <8ms render to leave room for everything else.

Edge cases

  • Context value identity: passing a fresh object every render forces all consumers to re-render.
  • List virtualization can interact badly with focus restoration in tables.

Real-world examples

  • Trading dashboards (price ticks), observability tools (Datadog, Honeycomb live views), Linear's real-time sync.

Senior engineer discussion

Discuss tear-free reads with `useSyncExternalStore`, why context isn't a state manager, and how to design the data flow so updates fan out to leaves without lifting state up.

Related questions