Back to React
React
medium
mid

How do you handle real time updates in a React application efficiently?

WebSocket (or SSE for one-way) → push updates into a normalized React Query cache or Zustand store. Dedup by id. On reconnect, fetch missed events since last timestamp. Memoize selectors; virtualize the list view. Backoff + jitter on reconnect. Use AbortController to cancel stale fetches. Optimistic UI for sends; reconcile when server confirms.

5 min read·~25 min to think through

Transport

ChoiceWhen
WebSocketBidirectional, low-latency (chat, multiplayer)
Server-Sent EventsServer→client only (notifications, ticker)
Long pollingFallback when SSE/WS blocked
PollingSimple cases, low update frequency
WebRTCP2P / real-time media

Most apps use WebSocket.

Connection management

ts
function connect(url: string, onMessage: (msg: any) => void) {
  let ws: WebSocket | null = null;
  let retry = 0;

  function open() {
    ws = new WebSocket(url);
    ws.onopen = () => { retry = 0; resync(); };
    ws.onmessage = (e) => onMessage(JSON.parse(e.data));
    ws.onclose = () => {
      const delay = Math.min(30_000, 1000 * 2 ** retry++ + Math.random() * 500);
      setTimeout(open, delay);
    };
  }

  function send(payload: any) { ws?.readyState === 1 && ws.send(JSON.stringify(payload)); }
  function close() { ws?.close(); }
  open();
  return { send, close };
}

Exponential backoff + jitter on reconnect. On open, resync — fetch missed events since last known timestamp.

Pushing into state

Option A — React Query cache:

ts
ws.onmessage = (msg) => {
  queryClient.setQueryData(['messages', channelId], (old) => mergeMessage(old, msg.payload));
};

Option B — Zustand store with normalized state:

ts
useChat.getState().applyMessage(msg.payload);

Normalized state ([[how-would-you-design-the-data-model-for-a-chat-application-ui-in-react]]) makes incremental updates trivial: id-keyed Map, insertion-sorted order array.

Dedup

WebSocket may echo back your own sent messages. Dedup by id (or tempId for optimistic):

ts
function applyMessage(state, m) {
  if (state.messages[m.id]) return state;     // already have it
  state.messages[m.id] = m;
}

Optimistic sends

ts
function send(body) {
  const tempId = uuid();
  applyMessage({ id: tempId, tempId, body, status: 'pending' });
  api.send({ tempId, body }).then((real) => replaceTemp(tempId, real));
}

Render efficiency

  • Memoize row components.
  • Virtualize the list.
  • Selectors with shallow equality.
  • Avoid re-rendering the whole list on a single message — incremental insert.

Cancellation

AbortController for fetches; close WebSocket on unmount / page hide.

Backpressure

If updates flood (typing indicators), throttle:

ts
const throttledTyping = throttle(applyTyping, 100);

Reconnect + resync

On reconnect:

  1. Fetch /events?since=lastSeenAt.
  2. Apply all missed events.
  3. Update lastSeenAt to latest.
  4. Resume realtime.

Watch for race: realtime events arriving during the resync — buffer them, replay after resync completes.

Visibility

Pause heavy updates when the tab is hidden:

ts
document.addEventListener('visibilitychange', () => {
  if (document.hidden) pause(); else resume();
});

Cross-tab

For multi-tab consistency, BroadcastChannel:

ts
const bc = new BroadcastChannel('app');
bc.onmessage = (e) => applyEvent(e.data);
bc.postMessage(event);

Or share one WebSocket in a SharedWorker if you have many tabs.

Interview framing

"WebSocket for bidirectional; SSE for server-only push. Connection layer with exponential backoff + jitter on reconnect, and a resync step that fetches missed events since lastSeenAt. Push messages into a normalized state (React Query cache or Zustand store), dedup by id. Optimistic sends use tempId; replace with real id when the server confirms. Render efficiency: virtualize the list, memoize rows, selector subscriptions. Throttle high-frequency events (typing). Pause when tab hidden. AbortController for cancellation. Cross-tab consistency via BroadcastChannel or a SharedWorker if needed."

Follow-up questions

  • How do you handle the resync race condition?
  • When would you use SSE over WebSocket?
  • How does optimistic UI work in this model?

Common mistakes

  • No dedup.
  • No backoff on reconnect — thundering herd.
  • No resync after disconnect.
  • Re-rendering the whole list per message.

Performance considerations

  • Normalize + memoize + virtualize. Throttle floods.

Edge cases

  • Reconnect during in-flight send.
  • Clock drift between server and client.
  • Multi-tab + same user.

Real-world examples

  • Slack, Discord, Linear, Notion, Liveblocks, Figma cursors.

Senior engineer discussion

Seniors design reconnect + resync as core, not afterthought, and choose the right transport per use case.

Related questions