Hedronite · Dev Synthesis Lesson · Web Stack (React + Three.js) · Sun 2026-07-05

Streaming Live Market Data into React and Three.js — Decoupling the Data Feed from the Render Loop, the useRef Escape Hatch, and Backpressure at the Scene-Graph Boundary

React state is for values a human changes. A market feed is not a human.

Lesson Class: Dev Synthesis (Web Stack · React + Three.js)
Framework Pair: React + Three.js (Sunday framework pair W1)
Arc: render loop (06-14) → Express-driven scene (06-28) → live-stream-driven scene (today)
Word Count: ~2,510
Grounding: Flanagan "JavaScript: The Definitive Guide" 7e §15.11.2 SSE pp.914-920 (grounded-in) + §15.11.3 WebSockets pp.921-922 (referenced)
Paired Ops: Real-Time Data Delivery to Web UIs
Paired Cert: Streaming Claude Responses to Production UIs (Anthropic Day)
Discipline: ROD v3 · water-accent code borders · HEDRONITE-AETHER-THEME v2.1 · clean code blocks (no inline comments)

§ IFrame

React's whole model is that state changes cause renders. That model is a trap the moment a live feed arrives faster than the screen refreshes. A market stream that pushes a hundred ticks a second, wired naively into useState, asks React to reconcile the tree a hundred times a second. The tab dies.

The 06-14 lesson built the render loop: a Three.js scene driven by requestAnimationFrame, painting at the frame's pace, with React managing the mount and the reconciliation boundary. The 06-28 lesson drove that scene from an Express API by polling. This lesson replaces the poll with a live stream, and in doing so runs straight into the problem the paired Ops lesson names: a producer faster than the consumer. The fix is not a faster React. The fix is to keep the fast data out of React entirely and let it meet the scene at a seam React does not watch.

§ IILanguage Idiom

The useRef escape hatch

React gives two ways to hold a value across renders. useState holds a value and, when it changes, triggers a re-render. useRef holds a value and, when it changes, triggers nothing. The ref is a mutable box React hands you and then ignores.

For streaming data, that ignoring is the feature. A hundred writes a second to a ref cost a hundred property assignments and zero renders. The ref becomes a store that lives beside the React tree rather than inside it. The EventSource handler writes to it; nothing in React notices, because nothing in React should.

This inverts the reflex. A React developer's instinct is that data belongs in state, and anything outside state is a code smell. For data that changes at network speed and must be drawn at frame speed, that instinct produces a frozen tab. The ref is the correct home precisely because it is invisible to reconciliation.

The two clocks

A streaming visualization runs on two clocks that must not be geared to each other.

The network clock ticks when an event arrives. It is irregular, bursty, and outside your control. The frame clock ticks when the browser is ready to paint, sixty times a second on a healthy tab, slower under load. It is regular and it is the only rate at which pixels actually change.

The naive design gears the two together: event arrives, state updates, React renders, scene repaints. Now the paint rate is the event rate, and the event rate can spike past what the machine can draw. The correct design ungears them. The event writes to the ref and returns. The frame reads the ref and draws. The two clocks share one variable and no control flow. That shared-variable-no-control-flow seam is the whole lesson.

§ IIICode Worked Example

The scene visualizes live ETF flow as a row of bars. Each ticker owns a bar; each event updates that ticker's target value; each frame eases the bar toward its target.

Start with the ref store and the stream. The store is a plain object keyed by ticker, holding the newest value and a liveness flag. The EventSource handler writes the newest value and overwrites any unrendered prior value, which is the client-side sampling the Ops lesson calls for: keep newest, drop intermediate.

function useFlowStream(url) {
  const store = useRef({ values: {}, live: false, asOf: 0 });

  useEffect(() => {
    const es = new EventSource(url);

    es.addEventListener("open", () => {
      store.current.live = true;
    });

    es.addEventListener("flow", (e) => {
      const tick = JSON.parse(e.data);
      store.current.values[tick.ticker] = tick.flow_1d_usd;
      store.current.asOf = tick.as_of_ms;
    });

    es.addEventListener("error", () => {
      store.current.live = false;
    });

    return () => es.close();
  }, [url]);

  return store;
}

Read the shape of this. The hook opens the stream once per URL, keyed by the useEffect dependency array. The handlers mutate store.current directly and return nothing to React. The open and error events flip the liveness flag the Ops lesson requires, so the scene can show connected-versus-reconnecting without a render. The cleanup closes the connection, which matters because a component that unmounts without closing leaks a live HTTP connection and, on reconnect churn, exhausts the browser's per-host connection budget. Flanagan's EventSource reconnects on its own after a drop, so the hook never writes reconnection logic; it only records the liveness transitions.

Now the render loop. It runs on the frame clock, reads the ref, and eases each bar toward its target. It never touches React state.

function useSceneLoop(store, barsRef) {
  useEffect(() => {
    let raf;
    const tick = () => {
      const bars = barsRef.current;
      const values = store.current.values;
      for (const ticker in bars) {
        const target = normalizeHeight(values[ticker] ?? 0);
        const bar = bars[ticker];
        bar.scale.y += (target - bar.scale.y) * 0.15;
        bar.position.y = bar.scale.y / 2;
        bar.material.color.setHex(target < 0 ? 0xd94040 : 0x2e8b57);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [store, barsRef]);
}

The loop reads whatever value is in the ref at the moment the frame fires. If forty events landed between two frames, thirty-nine of them were overwritten and never drawn, and that is correct: the fortieth is the current price, and the thirty-nine were on their way to it. The easing term — the 0.15 factor pulling scale.y toward its target — turns discrete jumps into smooth motion without any interpolation buffer, because the frame loop is already the interpolator. The position.y = scale.y / 2 line keeps each bar's base planted on the floor as it grows, the base-at-floor discipline from the 06-28 scene lesson.

The two hooks compose in the component, and React's job shrinks to mounting the canvas and wiring the refs:

function FlowScene({ streamUrl }) {
  const store = useFlowStream(streamUrl);
  const barsRef = useRef({});
  const mountRef = useRef(null);

  useEffect(() => {
    const { scene, camera, renderer, bars } = buildScene(mountRef.current);
    barsRef.current = bars;
    const render = () => {
      renderer.render(scene, camera);
      requestAnimationFrame(render);
    };
    render();
    return () => renderer.dispose();
  }, []);

  useSceneLoop(store, barsRef);

  return <div ref={mountRef} />;
}

React renders this component once. It mounts. After that, every pixel that changes on screen changes because a requestAnimationFrame callback read a ref and mutated a Three.js object, entirely outside React's knowledge. React is the stagehand that sets the scene; the two clocks run the show.

The Two-Clock Seam Network clock writes the ref; frame clock reads it. One shared variable, no shared control flow. Sample by overwriting so the newest value wins and the intermediate ones never cost a frame. The scene graph lives outside React, which is the only reason the whole design is possible.

§ IVConnection to Today's Ops Lesson

The Ops lesson names three backpressure disciplines: coalesce at the source, sample at the client, decouple receipt from render. This code implements the second and third. The sampling is the single line store.current.values[tick.ticker] = tick.flow_1d_usd — an overwrite, not an append, so intermediate values vanish by construction and no queue ever grows. The decoupling is the boundary between useFlowStream, which writes the ref at network speed, and useSceneLoop, which reads it at frame speed. The Ops lesson explains why the seam must exist; these two hooks are the seam.

The liveness clause maps directly too. The Ops lesson requires the UI to show whether the channel is connected. The open and error handlers write store.current.live, and the scene reads it each frame to dim the bars and freeze the timestamp when the stream drops. A frozen feed reads as frozen, exactly as the freshness contract demands, and it costs zero renders to keep honest.

§ VPrior-Lesson Reach

The 06-14 lesson established the render loop and the reconciliation boundary — the rule that Three.js objects live outside React's tree and are mutated imperatively inside a frame callback. This lesson depends on that rule completely: the ref store works only because the scene graph is already outside React. Had the bars been React components, a hundred events a second would still reconcile a hundred times a second no matter where the data lived.

The 06-28 lesson drove the scene from an Express API by fetching on an interval. That pattern polls: it asks repeatedly whether anything changed and mostly hears no. This lesson inverts the direction — the server pushes when something changes — and the fetchAndUpdate function from 06-28 survives, repurposed, as the resync path: when the stream falls outside the server's replay window, the client calls that same pull endpoint for a fresh snapshot, then resumes streaming. The poll did not disappear; it retired to the recovery path.

§ VIClosing

Hold streaming data in a ref, not in state. Let the EventSource handler write it at the network's pace and the requestAnimationFrame loop read it at the frame's pace, and never gear the two clocks together. Sample by overwriting, so the newest value wins and the intermediate ones never cost a frame. Keep the scene graph outside React so the whole design is even possible.

React state is for values a human changes. A market feed is not a human. Keep it out of the render cycle.

Take your last streaming component that stutters under load. Find the useState the feed writes to. Move it to a useRef and move the read into your frame loop. Watch the stutter go.

Paired lessons → Ops 01-Earth-DevOps/Synthesis-Lessons/2026-07-05-real-time-data-delivery-to-web-uis-... · Cert Cert-Prep/Anthropic/2026-07-05-streaming-claude-responses-to-production-uis-...

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Filed 2026-07-05 Sunday Fajr · Web Dev lesson (React + Three.js) · framework pair W1
Backward-Synergy-Reach → React+Three.js Render Loop (06-14) · Three.js from Express (06-28)
HTML shipped in-cycle per HARD DISCIPLINE · water-accent code borders · tome-grounded Flanagan §15.11.2-15.11.3