Zustand Transient Updates
For high-frequency socket data, write messages into a store and let only the leaf components subscribe — or skip React rendering entirely for the hottest values.
The fastest way to make a real-time React UI janky is to call setState from a WebSocket handler near the top of the tree.
Every message re-renders every component underneath — many times a second — and you see it as dropped frames, sluggish input
and memory that keeps climbing.
The problem
function Dashboard() {
const [prices, setPrices] = useState({});
useEffect(() => {
socket.onmessage = (e) => setPrices((p) => ({ ...p, ...JSON.parse(e.data) }));
}, []);
return <Layout prices={prices} />; // the whole layout re-renders on every tick
}
Intermediate components that merely pass the data down re-render too, even though nothing about them changed.
The fix, in four steps
- Own the socket once, at the app or parent level — not in each component that needs data.
- Write messages into a store. Zustand’s
useStore.setState(...)works outside React, so the socket handler doesn’t touch React state at all. - Subscribe with selectors where the value is rendered:
useStore((s) => s.prices[id]). A component only re-renders when its slice changes. Intermediate components no longer take the data as props, so they stop re-rendering. - Go transient for the hottest values. For tickers, cursors and anything updating many times a second, subscribe inside an effect and write straight to a ref or the DOM — no re-render at all.
// transient: update the DOM without re-rendering (subscribeWithSelector middleware)
useEffect(
() =>
useStore.subscribe(
(s) => s.prices[id],
(p) => {
if (ref.current) ref.current.textContent = p.toFixed(2);
},
),
[id],
);
Selector vs transient
| Selector subscription | Transient subscription | |
|---|---|---|
| Re-renders | only this component, when its slice changes | none |
| Good for | values that drive layout or several elements | text and numbers that just need to change on screen |
| Cost | React reconciliation per change | you own the DOM update |
Trade-offs
Transient updates step outside React’s model, so keep them small and local: a cell, a number, a sparkline. If a value changes what’s rendered (a row appears, a colour flips state), let React handle it. And a store is not a licence for one giant global blob — scope stores by feature.
In practice
On a real-time trading interface, moving all sockets to a single owner, writing into a store and deleting the setState
setters calmed the whole UI: only the leaf components showing prices updated. I’d learnt the same lesson earlier on an
enterprise platform, replacing a dual Redux-plus-localStorage state with a small store and narrow subscriptions. Different
domains, same rule: a store plus narrow subscriptions beats prop and setState cascades. The transport side of this lives
in WebSockets, and the architecture side in Bulletproof React.
- #zustand
- #react
- #state-management
- #performance
- #websockets
↩ Linked from (4)
Internal Developer Platform · Where Platforms Fit: DDD Subdomains · API Modelling · Bulletproof React · Zustand Transient Updates · AJAX Polling
- Message PassingGrowing
…-render. Thinking of the frontend as "a process with a mailbox" is what led me to the single-owner pattern in Zustand Transient Updates.
- ObservabilityGrowing
…ing screen can have perfect backend latency and still feel broken because the render loop can't keep up — see Zustand Transient Updates.
- WebSocketsEvergreen
…socket in one place, write messages into a store, and let only the leaf components subscribe — the pattern in Zustand Transient Updates.
↪ Links to (2)
Related by topic
- Growingcase study
Building a Graph Component Library
Frontend craft - Growing
Graph Visualisation Libraries
Real-time UIs - Growingcase study
Smart TV Apps on Low-End Devices
Frontend craft - Growingcase study
A Testing Strategy Teams Actually Adopted
Frontend craft