Skip to content
academia.sh

Lesson 11 / 25

Context Passing

Providing a value in the tree and reading it at any node below; the problem with prop drilling, the provider resolving upward, the nearest provider shadowing, the effect of the provided value's identity on consumer count, and where context does not fit.

Contents

All three storage forms from the previous lesson stay inside a single component. There is one known way for a value to reach another component: passed down from parent to child as a prop. On the measurement station page this path grows long. Whether the temperature shows as Celsius or Fahrenheit is chosen at the very top of the page; the place that needs this information is at the very bottom, in the measurement badge inside the table row. The four components in between — the page body, the measurement table, the table row, the cell — never use the unit, they only carry it.

This lesson establishes the mechanism that shortens the chain: providing the value at one node in the tree and reading it at any node below, without touching anything in between.

The Problem with Prop Drilling

Carrying a value through intermediate components is called prop drilling. The chain itself is not a mistake; for two or three levels, it is the most obvious solution. As it grows longer, it produces three separate costs.

The first is interface expansion. The measurement table’s API now has a unit parameter, even though the table has no concern with the unit at all. The component’s externally visible contract fills up with fields that say nothing about its own job.

The second is the cost of change. Adding a new value to the chain — the locale, the number of decimal places, threshold colors — means a signature change in every component in between. Adding three values to a five-level chain is touching fifteen files.

The third is reuse. A table forced to carry the unit cannot be used on a page that has no unit; and once the parameter is made optional, the question of where the default comes from arises.

Provider and Resolution

Context removes all three costs. A node in the tree provides a value under a key; any component below that node reads the value with the same key. The components in between are never aware of it.

Reading is a search: the component starts from its own node, climbs up the ancestor chain, and stops at the first node that provides that key. If no node provides it, the default value given along with the context’s definition is used.

// context-resolution.mjs — the provider being searched for upward through the tree
const node = (name, provided, children = []) => {
  const n = { name, provided, children, parent: null };
  for (const c of children) c.parent = n;
  return n;
};

const DEFAULTS = { unit: "kelvin" };   // used when no provider exists

function resolve(start, key) {
  let n = start;
  let steps = 0;
  while (n) {
    if (n.provided && key in n.provided)
      return { value: n.provided[key], provider: n.name, steps };
    n = n.parent;
    steps++;
  }
  return { value: DEFAULTS[key], provider: "(default)", steps };
}

const badge1 = node("badge-1");
const badge2 = node("badge-2");
const badge3 = node("badge-3");
const page = node("page", { unit: "celsius" }, [
  node("title"),
  node("filter-panel", null, [node("filter-list", null, [badge3])]),
  node("measurement-table", null, [
    node("row-1", null, [badge1]),
    node("comparison-panel", { unit: "fahrenheit" }, [badge2]),
  ]),
]);
const standaloneBadge = node("standalone-badge");   // outside the tree

console.log(`root: ${page.name}`);
for (const n of [badge1, badge2, badge3, standaloneBadge]) {
  const r = resolve(n, "unit");
  console.log(
    `  ${n.name.padEnd(17)} unit=${r.value.padEnd(10)}`,
    `source=${r.provider.padEnd(16)} ${r.steps} ancestor node(s) scanned`,
  );
}
root: page
  badge-1           unit=celsius    source=page             3 ancestor node(s) scanned
  badge-2           unit=fahrenheit source=comparison-panel 1 ancestor node(s) scanned
  badge-3           unit=celsius    source=page             3 ancestor node(s) scanned
  standalone-badge  unit=kelvin     source=(default)        1 ancestor node(s) scanned

The four rows show four separate behaviors. The first badge found the page provider three levels up; the table and row components in between never saw the unit.

The second badge is inside the comparison panel and stops at the provider one level up. The page provider is still there, but it is never searched for: the nearest provider wins. This makes it possible for one section of the same page to display with a different unit.

The third badge is on an entirely different branch of the tree and finds the page provider all the same. Context spreads through a subtree, not through a single branch.

The fourth badge is below no provider at all and falls back to the default. Choosing a meaningful default is a design task: a component that also works without context can be tested on its own and reused on another page.

Context Is Positional, Not Lexical

The most commonly misunderstood aspect of this mechanism is the point where it departs from the thing it resembles. Scope in the Programming Fundamentals course is lexical: the variables a function sees depend on where that function is written in the source code. Context, on the other hand, is positional: the value a component sees depends on where that component is placed in the tree.

The same measurement badge is written once in the source; it reads Celsius inside the table and Fahrenheit inside the comparison panel. The source code has not changed; the place has.

The nearest provider winning is the tree’s counterpart to the shadowing defined in the same course: the inner provider renders the outer one invisible within its own subtree. This power comes with a cost. Understanding which value a component will read is not enough from looking at its definition; its position in the tree has to be known. This is why values carried through context should be few in number and clearly named.

The Cost of Change

When the provided value changes, every component reading that context re-renders — wherever they are in the tree, even if the components in between have not changed. The provided value’s identity is therefore a direct cost item.

// context-cost.mjs — the effect of the provided value's identity on consumer count
const UNIT_CONSUMERS = 6;    // number of components reading the unit
const ROW_CONSUMERS = 2;     // number of components reading the selected row

const EVENTS = [
  { name: "panel opened",   unit: "celsius",    selected: null },
  { name: "row 1 selected", unit: "celsius",    selected: 1 },
  { name: "row 2 selected", unit: "celsius",    selected: 2 },
  { name: "unit changed",   unit: "fahrenheit", selected: 2 },
];

function run(strategy) {
  let previousUnit = "celsius";     // values set up on the first render
  let previousSelected = null;
  let total = 0;
  console.log(`${strategy}:`);
  for (const event of EVENTS) {
    let count = 0;
    if (strategy === "object built in the body") {
      count = UNIT_CONSUMERS + ROW_CONSUMERS;          // identity changes on every render
    } else if (strategy === "memoized single context") {
      const changed = event.unit !== previousUnit || event.selected !== previousSelected;
      count = changed ? UNIT_CONSUMERS + ROW_CONSUMERS : 0;
    } else {
      if (event.unit !== previousUnit) count += UNIT_CONSUMERS;
      if (event.selected !== previousSelected) count += ROW_CONSUMERS;
    }
    previousUnit = event.unit;
    previousSelected = event.selected;
    total += count;
    console.log(`  ${event.name.padEnd(14)} -> consumers re-rendered: ${count}`);
  }
  console.log(`  total: ${total}`);
}

console.log(`${UNIT_CONSUMERS} unit consumers, ${ROW_CONSUMERS} row consumers, 4 events`);
for (const s of ["object built in the body", "memoized single context", "two separate contexts"])
  run(s);
6 unit consumers, 2 row consumers, 4 events
object built in the body:
  panel opened   -> consumers re-rendered: 8
  row 1 selected -> consumers re-rendered: 8
  row 2 selected -> consumers re-rendered: 8
  unit changed   -> consumers re-rendered: 8
  total: 32
memoized single context:
  panel opened   -> consumers re-rendered: 0
  row 1 selected -> consumers re-rendered: 8
  row 2 selected -> consumers re-rendered: 8
  unit changed   -> consumers re-rendered: 8
  total: 24
two separate contexts:
  panel opened   -> consumers re-rendered: 0
  row 1 selected -> consumers re-rendered: 2
  row 2 selected -> consumers re-rendered: 2
  unit changed   -> consumers re-rendered: 6
  total: 10

The first strategy is the common style of rebuilding the provided value inside the provider’s body on every render. Even when the value’s content does not change, its identity does, so even the panel opening re-renders eight consumers. This is context’s counterpart to the referential stability problem from the previous lesson, and its fix is the same: the provided value is memoized.

The second strategy does this and filters out irrelevant renders; the thirty-two consumer renders drop to twenty-four. But every time the selected row changes, the six components reading the unit also re-render, because the two are carried in a single object.

The third strategy separates the values: the rarely-changing unit in one context, the frequently-changing selection in another. The total drops to ten. This yields the rule: values carried together in one context should change at the same frequency. If their change frequencies diverge, the context is split.

Where Context Does Not Fit

Context is not a state management solution; it is a transport path. The value is still held as state somewhere; context only lets it travel through the tree.

The values it suits share a common trait: they change rarely and concern a wide portion of the tree. Visual theme, measurement unit, locale, signed-in user information, formatting settings, and accessibility preferences fit this description.

Where it does not fit is just as clear. A frequently-changing value — scroll position, the row under the cursor, an animation’s progress value — re-renders every consumer on every change when carried through context. Setting up context for a single value shared between two components is more complex and more costly than lifting state up, from the previous lesson.

One last criterion is testability. A component that reads context falls back to the default value when rendered on its own. If the default is meaningful, the component can be tested independently; if there is no default and the absence of context produces an error, the component has to be wrapped in a provider on every test.

Summary

  • As prop drilling grows longer, it expands intermediate components’ interfaces, raises the cost of change, and makes reuse harder.
  • Reading context is a search that climbs from the consumer node up the ancestor chain and stops at the first node that provides that key; if none does, the default is used.
  • The nearest provider wins; the inner provider shadows the outer one within its own subtree.
  • Context is positional, not lexical: the value a component sees depends on its position in the tree, not the place of its definition.
  • If the provided value is rebuilt on every render, every consumer re-renders on every render; the value is memoized.
  • Values carried together in one context should change at the same frequency; if the frequencies diverge, the context is split.

Next Step

Context flows a value down through the tree. There is one more thing that flows in the reverse direction, and it has not been addressed up to this lesson: errors. What happens if a row in the measurement table throws an exception during render because of a corrupted record from the server? In a declarative tree, the answer to this question is harsher than expected: a half-finished tree is inconsistent, and the runtime has no information in hand to salvage it. The next lesson takes up the reasoning behind this behavior, the mechanism that isolates a subtree’s error from its surroundings, and the kinds of errors this mechanism cannot catch.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close