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

# 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.

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.

```js
// 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.

```js
// 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.
