Real-time
Draft1 min read
Managing real-time data with WebSockets in React
One connection, many subscribers: keeping WebSocket state outside React, reconnecting safely and rendering high-frequency updates without jank.
This article is a draft outline and has not been published yet.
WebSockets give an application a stream of updates instead of a series of requests. The difficulty in React is not opening the socket; it is keeping one connection healthy, sharing it across components, and rendering frequent updates without re-rendering the whole tree.
Keep the connection outside React
Components mount and unmount constantly. If each one opens its own socket, the app ends up with duplicate connections and missed messages. Create one connection manager as a plain module, and let components subscribe to it.
type Listener = (message: ServerMessage) => void;
class Connection {
private socket?: WebSocket;
private listeners = new Map<string, Set<Listener>>();
subscribe(channel: string, listener: Listener) {
const set = this.listeners.get(channel) ?? new Set();
set.add(listener);
this.listeners.set(channel, set);
return () => set.delete(listener);
}
private dispatch(message: ServerMessage) {
this.listeners.get(message.channel)?.forEach((listener) => listener(message));
}
}
Read it with useSyncExternalStore
React's useSyncExternalStore is designed for exactly this: reading from a store that lives outside React. It handles concurrent rendering correctly and only re-renders components whose selected value changed.
Keep the latest state per channel in the store, and give components selectors so that a price ticker does not re-render when a chat message arrives.
Reconnect deliberately
Connections drop. Plan for it:
- Reconnect with exponential backoff and random jitter, so thousands of clients do not reconnect at the same moment
- Re-subscribe to channels after reconnecting
- Ask the server for anything missed, using a sequence number or timestamp, instead of assuming nothing happened
- Show connection status in the interface when updates are stale
Rendering high-frequency updates
If messages arrive faster than the screen refreshes, rendering each one wastes work. Buffer incoming messages and apply them once per animation frame, or mark non-urgent updates with startTransition so typing and scrolling stay responsive.
For long lists of live data, combine this with virtualisation so only visible rows update.
Checklist
- One connection per tab, shared through a store.
- Components subscribe with selectors.
- Reconnect with backoff, re-subscribe and backfill.
- Batch high-frequency updates per frame.