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.
Transport
| Choice | When |
|---|---|
| WebSocket | Bidirectional, low-latency (chat, multiplayer) |
| Server-Sent Events | Server→client only (notifications, ticker) |
| Long polling | Fallback when SSE/WS blocked |
| Polling | Simple cases, low update frequency |
| WebRTC | P2P / real-time media |
Most apps use WebSocket.
Connection management
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:
ws.onmessage = (msg) => {
queryClient.setQueryData(['messages', channelId], (old) => mergeMessage(old, msg.payload));
};Option B — Zustand store with normalized state:
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):
function applyMessage(state, m) {
if (state.messages[m.id]) return state; // already have it
state.messages[m.id] = m;
}Optimistic sends
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:
const throttledTyping = throttle(applyTyping, 100);Reconnect + resync
On reconnect:
- Fetch
/events?since=lastSeenAt. - Apply all missed events.
- Update lastSeenAt to latest.
- 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:
document.addEventListener('visibilitychange', () => {
if (document.hidden) pause(); else resume();
});Cross-tab
For multi-tab consistency, BroadcastChannel:
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.