# Context API In Depth

Prerequisite: `frontend/react/hooks/03-usecontext` (the basic API, the default value, and the "all consumers re-render" gotcha). This file assumes you know `createContext` / `Provider` / `useContext` and goes after the mechanism, the composition patterns, and the escape hatches.

## 1. A Context Object Is Just a Pair of Fibers and a Stack

`createContext(defaultValue)` returns a plain object. Nothing magic lives in it:

```tsx
const ThemeContext = createContext<"light" | "dark">("light");

console.log(Object.keys(ThemeContext));
// ["$$typeof", "_currentValue", "_currentValue2", "Provider", "Consumer", ...]
```

The two fields that matter:

- `_currentValue` — a single mutable slot holding the value **currently in scope during rendering**. It is not per-component and not per-tree; it is one variable.
- `Provider` — a special element type React recognises. Rendering it does two things: it pushes the old `_currentValue` onto an internal stack and writes the new value into `_currentValue`; when React finishes that subtree it pops the stack back.

That push/pop is exactly why "nearest provider wins" works, and why it is a *render-time* lexical concept, not a runtime lookup:

```tsx
<ThemeContext.Provider value="dark">    {/* push "light", _currentValue = "dark" */}
  <A />                                  {/* reads "dark"  */}
  <ThemeContext.Provider value="light">  {/* push "dark",  _currentValue = "light" */}
    <B />                                {/* reads "light" */}
  </ThemeContext.Provider>               {/* pop -> _currentValue = "dark"  */}
  <C />                                  {/* reads "dark"  */}
</ThemeContext.Provider>                 {/* pop -> _currentValue = "light" (the default) */}
```

`useContext(Ctx)` does *not* climb the tree at call time. It reads `Ctx._currentValue` — which is already correct because React is depth-first rendering and the provider pushed the value on the way down.

## 2. What `useContext` Records on the Fiber

Reading a context is a *subscription*, and the subscription is stored on the reading fiber:

```tsx
function ThemedButton() {
  const theme = useContext(ThemeContext); // <- registers a dependency
  return <button className={theme} />;
}
```

During that call React appends an entry to `fiber.dependencies.firstContext`, a singly-linked list of `{ context, memoizedValue, next }`. The list is rebuilt from scratch on every render of that component — which is why conditional reads are *technically* fine for correctness even though `useContext` is a hook (it has no hook-slot; it doesn't participate in the hooks linked list at all). See `frontend/react/hooks/01-usestate` for the hooks list this is deliberately separate from.

## 3. Under the Hood: How a Provider Update Reaches Consumers

![Provider re-render walks its subtree, matches fiber dependency lists, and marks childLanes on every ancestor](diagrams/01-context-propagation.svg)

When a `Provider` fiber begins work, React compares the new `value` prop to the old one with `Object.is`:

1. **Equal** → nothing propagates. React can bail out of the whole subtree if nothing else scheduled work there.
2. **Not equal** → React calls `propagateContextChange`, which does a **manual depth-first walk of the provider's entire subtree**, looking at every fiber's `dependencies` list.

For each fiber whose dependency list contains this exact context object, React:

- schedules an update on that fiber (OR-s the current render lane into `fiber.lanes`), and
- walks from that fiber back **up** to the provider, OR-ing the lane into every ancestor's `childLanes`.

That second step is the whole story behind the most surprising context behaviour:

```tsx
const Sidebar = memo(function Sidebar() {
  return <ThemedButton />; // ThemedButton calls useContext(ThemeContext)
});

// <Provider value={newObject}><Sidebar /></Provider>
// Sidebar's props did not change. Sidebar still gets re-rendered.
```

`memo` (and `PureComponent`, and the "props are identical" fast path) works by *bailing out* — returning the previous children fibers untouched. But the bailout check is gated on `childLanes`: React refuses to bail out if any descendant has pending work. `propagateContextChange` already stamped `childLanes` on `Sidebar`, so React must descend through it to reach `ThemedButton`. **`memo` cannot stop context propagation.** It only avoids re-*executing* Sidebar's function body in some paths, and even that is not guaranteed — the common outcome is that Sidebar's function body runs again.

Three consequences that follow directly:

- **The comparison is `Object.is` on the whole `value` prop.** `value={{ user, theme }}` allocates a new object each render, so it is never equal, so every consumer in the subtree re-renders on every provider render. This is the memoize-the-value rule from `hooks/03-usecontext`, now with a reason attached.
- **There is no partial subscription.** React matches on the *context object identity*, not on which fields you read. A consumer that reads only `value.theme` is indistinguishable, at the fiber level, from one that reads `value.user`.
- **The walk is O(subtree size), not O(consumers).** A provider high in the tree with one deep consumer still pays a full subtree scan on every value change. React short-circuits subtrees containing another provider *for the same context* (they shadow it), but not otherwise.

## 4. Splitting Contexts: The Only Real Fix

Since React matches on context identity, the way to get finer-grained subscriptions is to have more context identities.

```tsx
// One context per independent concern
const UserContext = createContext<User | null>(null);
const ThemeContext = createContext<Theme>("light");

function AppProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [theme, setTheme] = useState<Theme>("light");
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
    </UserContext.Provider>
  );
}
```

Now a `setTheme` call propagates only through `ThemeContext`'s dependency matches. `UserContext`'s `value` prop is still the same `user` reference, so its provider bails out immediately.

Note there is no `useMemo` here at all — `user` and `theme` are already stable references straight out of `useState`. **Memoizing is only necessary when you build a composite object;** the better move is usually to not build one.

## 5. The State/Dispatch Split

A very common shape: a reducer whose `state` changes constantly but whose `dispatch` never changes. Putting both in one context forces every dispatcher-only component (buttons, forms) to re-render on every state change.

```tsx
const TodoStateContext = createContext<Todo[]>([]);
const TodoDispatchContext = createContext<React.Dispatch<Action>>(() => {});

function TodoProvider({ children }: { children: React.ReactNode }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return (
    <TodoDispatchContext.Provider value={dispatch}>
      <TodoStateContext.Provider value={todos}>{children}</TodoStateContext.Provider>
    </TodoDispatchContext.Provider>
  );
}

// A button that only dispatches never re-renders when todos change:
function AddButton() {
  const dispatch = useContext(TodoDispatchContext);
  return <button onClick={() => dispatch({ type: "add" })}>Add</button>;
}
```

`dispatch` from `useReducer` is guaranteed stable for the lifetime of the component (see `frontend/react/hooks/04-usereducer`), so `TodoDispatchContext`'s provider bails out on every render. Order the providers with the *stable* one outermost so a state change re-renders as little provider machinery as possible.

## 6. Custom Hook + Guard: The Standard Packaging

Exporting the raw context object leaks the "you forgot the Provider" failure mode into every call site. Export a hook instead, and use `undefined`-as-default to detect a missing provider:

```tsx
const AuthContext = createContext<AuthValue | undefined>(undefined);

export function useAuth(): AuthValue {
  const ctx = useContext(AuthContext);
  if (ctx === undefined) {
    throw new Error("useAuth must be used inside <AuthProvider>");
  }
  return ctx; // TypeScript now narrows to AuthValue — no `?.` at call sites
}
```

Two wins at once: a real error message instead of a silent `undefined.user` crash three components away, and the non-null narrowing that makes the consumer code clean.

## 7. Selector-Style Subscriptions with `useSyncExternalStore`

If you genuinely need field-level subscriptions inside one provider, stop using context to carry *changing values* and use it to carry an immutable *store handle*:

```tsx
type Store<T> = {
  getSnapshot: () => T;
  subscribe: (listener: () => void) => () => void;
};

const StoreContext = createContext<Store<AppState> | null>(null);

export function useSelector<S>(selector: (s: AppState) => S): S {
  const store = useContext(StoreContext)!;
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getSnapshot()), // re-read + re-select on every notify
  );
}
```

The context `value` is the store object, created once, so its identity **never changes** — `propagateContextChange` never fires. Updates flow through `subscribe`, and `useSyncExternalStore` re-renders a component only when its *selected slice* changes by `Object.is`. This is precisely the architecture Redux's `react-redux` and Zustand use; see `frontend/ecosystem/10-redux` and `frontend/ecosystem/12-zustand`.

## 8. Reading Context Conditionally with `use`

`use(Context)` (React 19) reads a context but, unlike `useContext`, may be called conditionally and inside loops:

```tsx
function Heading({ plain }: { plain: boolean }) {
  if (plain) return <h1>Title</h1>;
  const theme = use(ThemeContext); // legal — `use` is not slot-based
  return <h1 className={theme}>Title</h1>;
}
```

The subscription is registered when the call actually executes, so on renders where `plain` is `true` this fiber has no `ThemeContext` dependency and is skipped by propagation entirely. Useful for genuinely optional reads; it is not a performance trick for the common case.

## 9. Context and Server Components

`createContext` is a client-only API. A React Server Component has no fiber tree on the server for the provider stack to live in, so `useContext` and `Provider` both throw there. The pattern is to put the provider in a `"use client"` file and render it from a server component as a wrapper around `children`:

```tsx
// providers.tsx
"use client";
export function Providers({ children }: { children: React.ReactNode }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}

// app/layout.tsx — a Server Component
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html><body><Providers>{children}</Providers></body></html>;
}
```

`children` passed through a client component stays a server-rendered subtree — it does not get pulled into the client bundle. Details in `frontend/ecosystem/03-nextjs_server_components`.

## 10. Common Pitfalls

1. **Inline object/array/function as `value`.** `value={{ user, logout }}` and `value={[a, b]}` allocate a fresh reference every render, so `Object.is` always fails and every consumer in the subtree re-renders. Why it bites: the provider often re-renders for reasons unrelated to the context (a parent state change), so the cost appears with no visible cause.
2. **Reaching for `memo` to stop context re-renders.** It cannot work — `propagateContextChange` marks `childLanes` on the memo boundary, which is exactly the condition that forbids a bailout (Section 3). The fix is splitting contexts or moving to a store + selector.
3. **One "AppContext" holding everything.** Every unrelated field change re-renders every consumer. Why: matching is on context identity, so `settings.fontSize` and `user.name` are the same subscription.
4. **Using `createContext(defaultValue)` as the real value.** The default is used *only* when there is no matching provider anywhere above. A provider that renders `value={undefined}` gives you `undefined`, not the default. Why it bites: a typo in the provider surfaces as a mysterious `undefined` rather than a fallback.
5. **Putting a provider inside a component that re-mounts.** If the provider's parent conditionally renders it (`{open && <Provider>...}`), unmounting destroys all state held in the provider. Why: the state lives in the provider's fiber, and unmounting frees the fiber.
6. **Creating a context inside a component body.** `const Ctx = createContext(...)` inside render makes a *new* context object every render; consumers subscribe to old identities and read the default value forever. Always module scope.
7. **Assuming context is a state manager.** Context is a *transport* mechanism — it distributes a value; it does not store, batch, or diff anything. The state still lives in whatever `useState`/`useReducer`/external store you put above it.
8. **Deeply nested provider pyramids.** Ten nested providers means ten fibers on every render pass and ten stack push/pops. Harmless at small scale, but flatten with a composed `Providers` component and keep the stable ones outermost.
9. **Mutating the value object instead of replacing it.** `value.count++` keeps the same reference, `Object.is` passes, and no consumer updates. Why: propagation is identity-based, and React never deep-compares.

## 11. Quick Reference

| Need | Do this |
|---|---|
| Distribute a rarely-changing value | One context, value straight from `useState` |
| Distribute several independent values | One context **per concern**, nested providers |
| Reducer state + dispatch | Two contexts; dispatch provider outermost |
| Field-level subscriptions in one store | Context carries a stable store handle + `useSyncExternalStore` |
| Detect a missing Provider | `createContext<T \| undefined>(undefined)` + throwing custom hook |
| Optional / conditional read | `use(Context)` instead of `useContext(Context)` |
| Provider in a Next.js App Router tree | `"use client"` provider file, wrap `{children}` from the server layout |

| Mechanism | Fact |
|---|---|
| Value lookup | `Ctx._currentValue`, pushed/popped on a stack during render |
| Subscription storage | `fiber.dependencies.firstContext` linked list, rebuilt every render |
| Change detection | `Object.is(newValue, oldValue)` on the `value` prop |
| Propagation | Depth-first scan of the provider subtree (`propagateContextChange`) |
| Why `memo` doesn't help | Consumers' lanes are OR-ed into every ancestor's `childLanes`, forbidding bailout |
| Cost of a change | O(size of provider subtree), regardless of consumer count |

Next: `frontend/react/advanced/02-state_management_patterns` builds on this to compare context, reducer-per-feature, and external stores.
