---
title: 'Derived Values'
source: 'https://academia.sh/en/courses/component-based-development/derived-values'
course: 'Component-Based Interface Development'
language: en
updated: '2026-08-17T18:11:03+00:00'
license: 'CC BY-SA 4.0'
---

# Derived Values

Not holding in state a result computable from other values; the single source of truth criterion, the costs of writing a derivation into state, memoization's effect on recomputation count, the difference referential stability produces downstream, and memoization's own cost.

The previous lesson established the effect record as a tool for synchronizing with the
outside world, and its last sentence left a warning: the place it is set up most often is
not the outside world. Code that filters the measurement list when the selected type
changes in the filter panel, writes the result into a separate slot, and thereby triggers
a second render, is common. This lesson establishes why that pattern is wrong, what
replaces it, and what to do when the computation is genuinely expensive.

The distinction comes down to a single question: can this value be computed from other
values I already have? If it can, it is not state; it is a **derived value**.

## The Single Source of Truth

Five values circulate on the measurement station page: the measurement list from the
server, the type selected in the filter panel, the column the table is sorted by, the
filtered measurement list, and the count under the table that reads "17 of 42
measurements shown."

The first three are state; none can be computed from the others. The last two derive
entirely from the first three: the filtered list is a function of the measurements and
the selected type, and the count is the filtered list's length. Holding these two values
in separate slots amounts to storing the same information in two places.

Holding the same information in exactly one place is called the **single source of
truth**. The criterion is plain: before storing a value, ask whether it could instead be
computed. If it can be computed, it is not stored.

This criterion has a hidden consequence. When the filtered list is not held in state, it
is **impossible to forget** to update the filtered list when a new row is added to the
measurement list; the filtered list does not exist ahead of time at all, it is recomputed
on every render. In a design that stores the derived value, the responsibility for
updating it shifts to the programmer, and every path that gets forgotten produces an
inconsistency.

## Three Costs of Writing a Derivation into State

The pattern of computing a derived value with a side effect and writing it into state
pays three separate costs.

**An extra render.** The sequence is: the user changes the type, state updates, the body
runs, the tree is written to the document, the effect runs, it writes the filtered list
into state, the body reruns, the tree is written again. A single click produces two full
rounds.

**An inconsistent intermediate frame.** Between these two rounds there is a frame on
screen where the selected type is new but the list is still old. A user who selected
"temperature" sees humidity measurements for a moment. This frame is short enough to go
unnoticed; on a slow device, it does not.

**Drift.** The measurement list can also change through another path — new data arrives
from the server, a row is deleted. If the effect's dependency list does not cover this
path, the filtered list stays stale, and the two sources drift apart permanently.

The correct form is to put the computation inside the body. The body recomputes the
filtered list on every render; the separate slot, the extra render, the intermediate
frame, and the drift all disappear at once. The only remaining question is the cost of
the computation.

## Memoization

Filtering and sorting are too cheap to measure for a few hundred rows. With tens of
thousands of rows, and a date parse or a unit conversion per row, the computation becomes
visible. At that point, the target is not the computation itself but its **unnecessary
repetition**.

Storing and returning a derived value's previous result for as long as its inputs have
not changed is called **memoization**. This is the same technique introduced under the
name "note-taking" in the Programming Fundamentals course to speed up recursive
computations; the difference here is that the stored input is a dependency list.

```js
// memoization.mjs — memoizing a derived value and the recomputation count
const MEASUREMENTS = [
  { code: "NS-01", type: "temperature", value: -4.2 },
  { code: "NS-02", type: "humidity", value: 68 },
  { code: "NS-03", type: "temperature", value: 1.5 },
  { code: "NS-04", type: "wind", value: 12.4 },
  { code: "NS-05", type: "temperature", value: -0.8 },
];

let computeCount = 0;
function filterAndSort(measurements, type) {
  computeCount++;
  return measurements
    .filter((m) => type === "all" || m.type === type)
    .sort((a, b) => a.value - b.value)
    .map((m) => m.code);
}

function memoized(slots, i, compute, deps) {
  const slot = slots[i];
  const same = slot && slot.deps.length === deps.length
    && slot.deps.every((v, k) => Object.is(v, deps[k]));
  if (same) return slot.result;
  const result = compute();
  slots[i] = { deps, result };
  return result;
}

// There are also renders that do not change the type: opening and closing the panel changes state.
const TYPES = [
  ["all", false], ["all", true], ["all", false],
  ["temperature", false], ["temperature", true], ["humidity", true],
];

for (const memoize of [false, true]) {
  computeCount = 0;
  const slots = [];
  console.log(memoize ? "memoized:" : "computed on every render:");
  for (const [type, open] of TYPES) {
    const list = memoize
      ? memoized(slots, 0, () => filterAndSort(MEASUREMENTS, type), [MEASUREMENTS, type])
      : filterAndSort(MEASUREMENTS, type);
    console.log(`  type=${type.padEnd(11)} panel=${String(open).padEnd(5)} list=${list.join(",")}`);
  }
  console.log(`  6 renders, ${computeCount} computations`);
}
```

```
computed on every render:
  type=all         panel=false list=NS-01,NS-05,NS-03,NS-04,NS-02
  type=all         panel=true  list=NS-01,NS-05,NS-03,NS-04,NS-02
  type=all         panel=false list=NS-01,NS-05,NS-03,NS-04,NS-02
  type=temperature panel=false list=NS-01,NS-05,NS-03
  type=temperature panel=true  list=NS-01,NS-05,NS-03
  type=humidity    panel=true  list=NS-02
  6 renders, 6 computations
memoized:
  type=all         panel=false list=NS-01,NS-05,NS-03,NS-04,NS-02
  type=all         panel=true  list=NS-01,NS-05,NS-03,NS-04,NS-02
  type=all         panel=false list=NS-01,NS-05,NS-03,NS-04,NS-02
  type=temperature panel=false list=NS-01,NS-05,NS-03
  type=temperature panel=true  list=NS-01,NS-05,NS-03
  type=humidity    panel=true  list=NS-02
  6 renders, 3 computations
```

The two sections' **lists are identical line by line**; memoization does not change the
result, only the number of times it gets produced. In three of the six renders, the type
is the same as the previous one — opening and closing the panel produces a render but
does not concern the filter — and the memoized form returned the stored result on those
three occasions.

The memoization slot holds a single input. If "all" were selected again after the last
round, the computation would run from scratch, because the intervening rounds had already
filled the slot. This is not a shortcoming but a deliberate limit: an unbounded cache
holds memory for every distinct input combination and requires an eviction policy. A
single-step window carried from one render to the next already covers typical usage.

## Referential Stability

Memoization's second justification comes not from speed but from identity, and it is
less well known. If the derived value is an array or an object, the result rebuilt on
every render has **the same content but a different identity**. This difference
propagates downward: the dependency list of the component receiving the result looks
changed on every render.

```js
// identity.mjs — the downstream effect of a derived value's identity
const MEASUREMENTS = [
  { code: "NS-01", type: "temperature" },
  { code: "NS-02", type: "humidity" },
  { code: "NS-03", type: "temperature" },
];

const temperatureReadings = (source) => source.filter((m) => m.type === "temperature");

function memoized(slots, i, compute, deps) {
  const slot = slots[i];
  if (slot && slot.deps.length === deps.length && slot.deps.every((v, k) => Object.is(v, deps[k]))) return slot.result;
  const result = compute();
  slots[i] = { deps, result };
  return result;
}

for (const memoize of [false, true]) {
  const slots = [];
  let previousList = null;   // the value the child component keeps in its dependency list
  let effectCount = 0;
  console.log(memoize ? "memoized derivation:" : "re-derived on every render:");

  for (let n = 1; n <= 4; n++) {          // four renders, inputs never change
    const list = memoize
      ? memoized(slots, 0, () => temperatureReadings(MEASUREMENTS), [MEASUREMENTS])
      : temperatureReadings(MEASUREMENTS);
    const sameObject = Object.is(list, previousList);
    const deepEqual = previousList !== null
      && list.length === previousList.length
      && list.every((m, k) => m === previousList[k]);
    if (!sameObject) effectCount++;         // the child's effect reruns
    console.log(
      `  render ${n}: same object=${String(sameObject).padEnd(5)}`,
      `same content=${previousList === null ? "-    " : String(deepEqual).padEnd(5)}`,
      `child effect ran=${!sameObject}`,
    );
    previousList = list;
  }
  console.log(`  4 renders, child effect ran ${effectCount} time(s)`);
}
```

```
re-derived on every render:
  render 1: same object=false same content=-     child effect ran=true
  render 2: same object=false same content=true  child effect ran=true
  render 3: same object=false same content=true  child effect ran=true
  render 4: same object=false same content=true  child effect ran=true
  4 renders, child effect ran 4 time(s)
memoized derivation:
  render 1: same object=false same content=-     child effect ran=true
  render 2: same object=true  same content=true  child effect ran=false
  render 3: same object=true  same content=true  child effect ran=false
  render 4: same object=true  same content=true  child effect ran=false
  4 renders, child effect ran 1 time(s)
```

The input did not change in any of the four renders. In the unmemoized form, the "same
content" column reads true on every line while the "same object" column reads false on
every line: the list is an equivalent but different array every time. The effect in the
component below counts this as a change and runs four times. In the memoized form it runs
once.

This is called **referential stability**, and it is the real reason for memoization
whenever a derived value crosses a component boundary. Making the comparison an identity
comparison is a design choice: a deep comparison costs more than the computation itself
on large structures. Its price is that preserving identity becomes the caller's
responsibility.

The same reasoning applies to functions. A callback written inside the body is a new
function object on every render; when passed down, it looks changed every time.
Memoizing the function solves the same problem as memoizing the array.

## Memoization's Own Cost

Memoization is not free. Every memoized value holds a slot, a list comparison runs on
every render, and the stored result stays in memory. For a filtered array of three
items, this cost is larger than the computation itself.

Three criteria are enough to decide. If the computation is genuinely expensive —
measured, not guessed — it is memoized. If the result crosses a component boundary and
feeds into a dependency list below, it is memoized. If neither holds, it is not memoized.

Memoizing without measuring produces a body that is hard to read and a gain that cannot
be measured. The performance recording from The Browser and the Web Platform course is
the only source that tells which computation is genuinely expensive; dependency lists are
added by consulting that recording.

## Summary

- A value computable from other values is not state; it is not stored, it is derived on
  every render.
- Writing a derivation into state with a side effect produces an extra render, an
  inconsistent intermediate frame, and the risk that the sources drift apart.
- Memoization returns the previous result for as long as the dependency list has not
  changed; it does not change the result, it reduces the recomputation count.
- The memoization slot holds a single-step window; returning to an old input makes the
  computation run again.
- Referential stability prevents dependency lists downstream from appearing to change
  needlessly when a derived array or function crosses a component boundary.
- Memoization carries the cost of a slot, a comparison, and memory; it is not added for
  an unmeasured computation.

## Next Step

Derived values are recomputed on every render, while state lives across renders and
starts a new render when it changes. There is a gap between these two categories. A
timer's identity, a request's sequence number, an element's previous position, the very
node that scrolls the table — all of them need to live across renders, yet none of their
changes produces a change on screen. Writing them into state triggers an unnecessary
render on every change; holding them in a local variable means losing them on the second
render. The next lesson defines this third category.
