C.W.K.
Stream
Lesson 04 of 05 · published

One Socket Owner, Many Listeners

~11 min · frontend-architecture, single-owner, events, react

Level 0Open Gate
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Every component that opens its own socket is a small leak waiting to happen. Own the connection once; let the rest just listen."

The stream is app-level, not per-component

Keep has several workspaces that all want live prices — the portfolio view, the market pulse, a focused ticker. The tempting React pattern is for each of those components to open the WebSocket it needs. Keep forbids that. A single app-level hook (useMarketStream) owns the only WebSockets. Every workspace listens to broadcast events (a keep-market-update event) rather than opening its own connection. One owner, many listeners.

Why per-component sockets are a mess

Let each component open its own socket and you sign up for a pile of problems. Now three components hold three connections to the same provider hub, tripling the load. Each has its own mount/unmount lifecycle, so sockets leak when a component unmounts without cleaning up, or reconnect-storm when a view remounts. The components can disagree about the current price because their sockets received ticks at slightly different moments. And 'pause' becomes a nightmare — you have to find and stop every component's socket instead of one. Per-component connections turn one clean concern into N tangled ones.

A shared external resource should have exactly one owner. Connections, subscriptions, timers, and streams want a single lifecycle, not one per consumer. Give the resource one owner and broadcast its output; consumers subscribe to data, never to the connection. This collapses N lifecycles into one and makes 'stop everything' a single action.

The bridge and the overlay

Underneath, the flow has a tidy shape. A broadcast tick first updates a thread-safe, process-local price bridge — a small in-memory store of the newest values. Then the REST projections (portfolio, watchlist, focus, pulse, export) overlay that newer value on top of their durable data when they render. So live and durable aren't two competing sources fighting in the UI; live is a thin, fresh layer painted over the stable base at read time. The socket owner keeps the bridge current; everyone else reads the bridge-over-durable composite.

Don't reintroduce a socket inside a workspace 'just for this one view.' It always starts as a shortcut — one component needs data a little faster, so it opens its own connection. Now you have two owners, and the whole single-owner guarantee is gone: double load, split lifecycles, and a pause that misses one. If a view needs the stream, it listens to the app-level owner; it never opens its own.
This is the same 'one owner' instinct as the single DB writer. On the backend, one process owns all writes; on the frontend, one hook owns all sockets. Both dissolve a category of concurrency bug by refusing to have multiple owners of a shared resource. The pattern rhymes across the stack: centralize the thing everyone shares, broadcast its result.

Code

One hook owns the sockets; workspaces only listen·typescript
// App level: the ONLY place sockets are opened.
function useMarketStream() {
  useEffect(() => {
    const socket = openSocket(`/ws/stocks?live=1`);
    socket.onMessage = (tick) => {
      priceBridge.update(tick);                       // freshest value store
      window.dispatchEvent(                            // broadcast to listeners
        new CustomEvent("keep-market-update", { detail: tick }));
    };
    return () => socket.close();                       // one lifecycle to clean up
  }, []);
}

// A workspace LISTENS — it never opens its own socket.
function useLivePrice(ticker: string) {
  const [v, setV] = useState(durableClose(ticker));
  useEffect(() => {
    const on = (e: CustomEvent) => { if (e.detail.ticker === ticker) setV(e.detail.value); };
    window.addEventListener("keep-market-update", on as EventListener);
    return () => window.removeEventListener("keep-market-update", on as EventListener);
  }, [ticker]);
  return v;   // durable close, overlaid by the live bridge value
}

External links

Exercise

Imagine three components that each need the same live feed. Sketch the per-component version (each opens its own socket) and list every problem it creates: load, leaks, disagreement, pause complexity. Then refactor to one owner that broadcasts, with components listening. Explain what 'pause everything' costs in each design — one action versus N.
Hint
The per-component version fails most visibly on cleanup and pause: unmount one component and does its socket actually close? Pause the app and did you catch every component's connection? A single owner makes both trivial — one close(), one stop — because there was only ever one connection to manage.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.