Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    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.

    Evergreentended planted 2 min read
    On this page

    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

    1. Own the socket once, at the app or parent level — not in each component that needs data.
    2. Write messages into a store. Zustand’s useStore.setState(...) works outside React, so the socket handler doesn’t touch React state at all.
    3. 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.
    4. 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 subscriptionTransient subscription
    Re-rendersonly this component, when its slice changesnone
    Good forvalues that drive layout or several elementstext and numbers that just need to change on screen
    CostReact reconciliation per changeyou 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

    ↪ Links to (2)