Typorithm · hard

9. Context ownership and provider invariants

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

9. Context ownership and provider invariants

0%

Concept and contract

Expose a small shared capability and fail clearly when its provider is missing.

Context transports a value through the component tree; it does not automatically create ownership, persistence or a global store. Use it when several descendants need the same capability and prop passing has become inconvenient. Keep local form state local unless it truly has a shared owner.

A nullable default can accurately represent the absence of a provider. A custom hook can then check that invariant and return a nonnullable capability. Avoid inventing a fake default implementation that silently makes missing providers appear to work.

Mechanism and worked example

The example defines a Saved capability with a readonly id list and a toggle operation.

The example defines a Saved capability with a readonly id list and a toggle operation. The custom hook reads context and throws a clear error when no provider is present. Consumers receive the checked capability without repeating assertions.

The provider still needs a real state implementation. Changing its value can rerender consumers, so keep the contract focused and measure before adding memoization. A stable type does not make every newly constructed provider object referentially equal.

Complete worked example

typescript

import {createContext,useContext} from "react";
type Saved={ids:readonly string[];toggle:(id:string)=>void};
const SavedContext=createContext<Saved|null>(null);
export function useSaved():Saved{const value=useContext(SavedContext);if(value===null)throw new Error("Saved provider required");return value;}
Reasoning
The hook establishes the provider invariant before exposing a nonnullable shared capability.

Counterexamples and common mistakes

The rejected assignment omits the toggle capability required by the context contract.

The rejected assignment omits the toggle capability required by the context contract. A cast would hide a consumer-visible missing operation. Likewise, a non-null assertion on useContext bypasses the missing-provider check and can produce a less useful runtime failure later.

Do not assume context persists across process restarts or browser reloads. Persistence needs an explicit storage layer and account-scoping policy. Sensitive state should not be copied into a broadly accessible context simply because many components can read it.

Rejected counterexample: expected compiler diagnostic

typescript

type Saved={ids:readonly string[];toggle:(id:string)=>void};
// @ts-expect-error Consumers require a real toggle capability.
const invalid:Saved={ids:[]};
Reasoning
A context type should describe operations consumers can actually invoke.

Independent practice

Implement a pure requireSaved helper that accepts a nullable capability and returns the capability or throws.

Implement a pure requireSaved helper that accepts a nullable capability and returns the capability or throws. Test it independently before using it in a custom hook. Keep the supplied object identity unchanged on success.

Then wire the helper into a context hook and mount a provider harness. Optional extension: persist saved ids through an account-scoped store. The basic task is only the provider invariant; persistence is a separate feature with its own migration and privacy requirements.

Step 1: Check for a missing capability. Step 2: Return the supplied capability unchanged.

Practice starter: solve before reading the solution

typescript

type Saved={ids:readonly string[];toggle:(id:string)=>void};
function requireSaved(value:Saved|null):Saved{throw new Error("TODO");}
Reasoning
Test the small invariant helper before adding framework orchestration.
  • Hint 1: A fake default can conceal missing providers.
  • Hint 2: Context does not automatically persist data.

Solution, tests and trade-offs

The solution makes the invariant explicit with one branch and returns the original capability.

The solution makes the invariant explicit with one branch and returns the original capability. This is simpler than a generic assertion helper when only one domain context needs it. The React wrapper can call it after useContext without weakening the contract.

Runtime tests cover present and absent values. A component harness should verify the consumer works under a provider and fails clearly without it. Type checking alone cannot establish where the component is mounted in the actual tree.

Practice solution and acceptance checks

typescript

type Saved={ids:readonly string[];toggle:(id:string)=>void};
function requireSaved(value:Saved|null):Saved{
  if(value===null)throw new Error("Saved provider required");
  return value;
}

// Acceptance checks
const x:Saved={ids:[],toggle:()=>{}};if(requireSaved(x)!==x)throw new Error("identity");let bad=false;try{requireSaved(null)}catch{bad=true}if(!bad)throw new Error("missing provider");
Reasoning
The check preserves the original capability and gives missing-provider failure a clear meaning.

Interview reasoning and next step

In interviews, distinguish context transport, state ownership and persistence.

In interviews, distinguish context transport, state ownership and persistence. Explain why a nullable default plus a checked hook can be preferable to a fake no-op default. Discuss rerenders as runtime behavior rather than a type-system guarantee.

Next, return to module and package contracts so shared capabilities can cross project boundaries without accidental runtime imports, unresolved aliases or private implementation leakage.

Optional stretch — after the core task works: Add an account-scoped persistence adapter.

Expose a small shared capability and fail clearly when its provider is missing.