Skip to content
academia.sh

Lesson 10 / 25

References

Values that live across renders but do not start a re-render when they change; the two axes of storage forms, the box filling during the commit phase, the rule against touching the box during a render, and passing access to a document node across a component boundary.

Contents

The previous two lessons established two categories. State lives across renders and starts a new render when it changes. A derived value is never stored; it is recomputed on every render. On the measurement station page there are values that fall into neither category.

A counter that counts scroll events, the sequence number of a sent request, a timer’s identity, the filter type from the previous render, the document node that holds the table itself. What they all have in common is this: they need to live across renders, but their changing does not change anything on screen. This lesson defines the third category and calls it a mutable reference. For short, it will be called a box: a container tied to the component instance that holds a single field.

Two Axes

Deciding where to hold a value comes down to two independent questions: does the value survive into the next render, and does changing it start a render?

Storage form Survives across renders Changing it starts a render
Local variable inside the body no no
State slot yes yes
Mutable reference yes no
Module-level variable yes no

Three of the four rows serve distinct needs. A local variable is for holding an intermediate result within a single render. A state slot is for everything visible on screen. A reference is for values that need to live but stay invisible.

The fourth row — the module-level variable — gives the same two answers as the reference, but with a difference: it is tied to the module, not the component. If a page has two instances of the same component, both share the same variable. When a page has two measurement tables, holding the scroll counter at module level mixes the two tables’ counters together. A reference is tied to the instance; each table gets its own box.

Holding the Same Counter in Three Different Places

The difference is measurable. The measurement table counts scroll events but updates the on-screen indicator only every fifth event. The same requirement is met with three storage forms.

// storage.mjs — the result of holding the same counter in three different places
const EVENTS = ["s", "s", "s", "o", "s", "s", "s", "s", "s", "o", "s", "s"];
const THRESHOLD = 5;

function scenario(form) {
  let renderCount = 1;           // the first render
  let local = 0;                 // local variable inside the body
  let state = 0;                 // state slot
  const box = { value: 0 };      // mutable reference
  let onScreen = 0;

  const render = () => { renderCount++; local = 0; };   // local resets on every render

  for (const event of EVENTS) {
    if (event === "o") { render(); continue; }          // a state change unrelated to the counter
    if (form === "local") {
      local++;
      if (local % THRESHOLD === 0) { onScreen = local; render(); }
    } else if (form === "state") {
      state++;
      onScreen = state;
      render();                                         // every write produces a render
    } else {
      box.value++;
      if (box.value % THRESHOLD === 0) { onScreen = box.value; render(); }
    }
  }

  const counter = form === "local" ? local : form === "state" ? state : box.value;
  console.log(
    `${form.padEnd(5)} | counter=${String(counter).padStart(2)}`,
    `| onScreen=${String(onScreen).padStart(2)} | render=${renderCount}`,
  );
}

console.log("10 scrolls + 2 unrelated state changes; the screen updates every 5th scroll");
for (const f of ["local", "state", "box"]) scenario(f);
10 scrolls + 2 unrelated state changes; the screen updates every 5th scroll
local | counter= 2 | onScreen= 5 | render=4
state | counter=10 | onScreen=10 | render=13
box   | counter=10 | onScreen=10 | render=5

The three rows show three different results.

The local variable loses the count. Ten scrolls have been counted, yet the counter stops at two, because every intervening render — the two renders the counter itself triggered and the two coming from the filter panel — reset the variable. This is not a shortcoming of the local variable but its definition.

The state slot counts correctly, but produces thirteen renders. Every increment of the counter means a render; yet the screen changed in only two of those increments. Eleven renders’ output is identical to the one before it.

The reference box counts correctly and produces five renders: the two renders where the screen genuinely changed, the two coming from the filter panel, and the first render. This states in a single line why the third category exists.

When the Box Fills

The second use of a reference is reaching the document node a component produces. Focusing a node, measuring it, or handing it to an observer requires the node itself; yet the component body produces not nodes but a description of nodes. The link is established by the runtime filling the box.

// when-the-box-fills.mjs — when the node box fills, and the "previous value" pattern
const box = (initial) => ({ value: initial });

const nodeBox = box(null);        // the table node in the document will be written here
const previousType = box(null);   // the type from the previous render

let renderCount = 0;

function body(type) {
  renderCount++;
  const changed = previousType.value !== null && previousType.value !== type;
  console.log(`render ${renderCount} (type=${type})`);
  console.log(`   body reads -> node box     : ${nodeBox.value}`);
  console.log(`   body reads -> previous type: ${previousType.value}, changed: ${changed}`);
  return { tag: "table", key: `table-${type}` };
}

function commitToDocument(tree) {
  const node = `[node ${tree.tag} ${tree.key}]`;
  nodeBox.value = node;           // the runtime fills the box
  console.log(`   commit phase -> box = ${node}`);
}

function effects(type) {
  console.log(`   effect reads -> ${nodeBox.value} can be measured`);
  previousType.value = type;      // stored for the next render
}

for (const type of ["temperature", "temperature", "humidity"]) {
  const tree = body(type);
  commitToDocument(tree);
  effects(type);
}
render 1 (type=temperature)
   body reads -> node box     : null
   body reads -> previous type: null, changed: false
   commit phase -> box = [node table table-temperature]
   effect reads -> [node table table-temperature] can be measured
render 2 (type=temperature)
   body reads -> node box     : [node table table-temperature]
   body reads -> previous type: temperature, changed: false
   commit phase -> box = [node table table-temperature]
   effect reads -> [node table table-temperature] can be measured
render 3 (type=humidity)
   body reads -> node box     : [node table table-temperature]
   body reads -> previous type: temperature, changed: true
   commit phase -> box = [node table table-humidity]
   effect reads -> [node table table-humidity] can be measured

The sequence has three stages: the body runs, the tree is written to the document, the effects run. The box fills in the second stage. This is why the box is empty in the first render’s body — the node to be measured does not exist yet — and full in the effect. Code that measures a node, focuses it, or hands it to an observer is therefore written in an effect record.

The third render’s body shows a second detail: at that moment, the box is carrying the node from the previous render. While the body runs, the old tree still sits in the document. This is not staleness in the box but the order of the stages.

The second box establishes a different pattern. A box written in an effect and read in the body carries the value from the previous render and makes comparison possible. In the third render, “changed” came out true: the body knows the type changed in this round only because of the box. This is called the previous value pattern.

Not Touching the Box During a Render

The binding rule: the body neither reads nor writes a box. Touching a box is free in effects, event handlers, and timer callbacks.

The reasoning has three parts. The body’s contract is purity, and writing to a box is a side effect. The body can be interrupted midway; a value an interrupted body wrote to a box is left over from a render that is never shown. While the body runs, the document still carries the previous tree; a node box that is read gives back the old node.

The rule’s only exception is setting a box’s initial value once: filling it when empty and leaving it untouched once full does not break purity, because it produces no different result on any render.

Access to a Document Node and the Component Boundary

The legitimate uses of holding a reference to a node are countable: moving focus to an element, reading an element’s measurements, attaching an observer to a target, playing a media element, handing a container to a library written outside the component’s control. Everything outside this list — adding a class, writing text, hiding a child — is done declaratively, that is, from state. Code that mutates the node by hand produces a change that reconciliation overwrites on the next render, and the two update sources collide.

A reference crossing a component boundary is a separate design decision. A parent component may want access to the node a child produces: when the filter panel closes, focus needs to return to the first row in the table. Exposing the raw node breaks encapsulation; code outside the component becomes bound to its internal structure — which tag is used, how many wrappers there are.

A more robust form exposes a command surface, not the node: the component hands out named operations like focus and scrollToTop, and keeps the node to itself. When the internal structure changes, only the component changes. This is the interface–implementation distinction from the Programming Fundamentals course, applied to components.

When State, When a Box

The decision is made with a single question: when this value changes, should anything change on screen?

If it should, that value is state; writing it to a box silently leaves the screen stale and produces a class of bug that is hard to find — the data is correct, the view is stale. If it should not, it is a box; writing it to state produces an unnecessary render on every change.

Two in-between cases come up often. If a value concerns the screen only some of the time, it is split in two: the raw counter in a box, the threshold value shown on screen in state — as in the example above. If a value is read only in an event handler, it is a box; no render reads it.

Summary

  • Storage forms split along two axes: surviving across renders, and starting a render when they change.
  • A mutable reference survives but does not start a render; its difference from a module-level variable is that it is tied to the component instance.
  • The same counter is lost when held in a local variable, produces unnecessary renders when held in state, and produces neither problem when held in a box.
  • The box fills during the commit phase; while the body runs, it carries the previous render’s node, which is why code that touches the node is written in an effect.
  • The body neither reads nor writes a box; the only exception is setting the box’s initial value once.
  • A node reference is for focus, measurement, and binding to an external library; what crosses the boundary is named commands, not the raw node.

Next Step

All three storage forms defined so far stay inside a single component. There is exactly one way for a value to reach another component: passed down from parent to child. On the measurement station page there is a case where this path grows long. Whether the temperature unit shows as Celsius or Fahrenheit is chosen at the very top of the page, but the place that needs this information is at the very bottom, in the measurement badge inside the table. None of the four components in between uses the unit; they only carry it. The next lesson takes up the mechanism that shortens this chain: providing a value in the tree and reading it at any node below, without touching the components in between.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close