Type a React context provider and its consumer
Canonical URL: https://devexamples.com/typescript/type-a-react-context-provider/
The problem
Several components in one tree need the same design tokens, and threading them as props reaches three levels deep. The value must be typed, but during development a subtree is sometimes rendered without its provider, and reading properties from that undefined result is what usually gets noticed first in the console rather than in the compiler.
Short answer
Create the context with the value type unioned with null, render a typed provider component, and expose a hook that narrows away the null with one explicit error so consumers see the finished value type.
Context is the smallest place where TypeScript and React both have an opinion. The type parameter says what a consumer receives; the sentinel says what happens when no provider exists. Declaring both explicitly keeps the second case in the code you wrote rather than in a runtime error.
// src/theme-context.tsx
import { createContext, useContext, type PropsWithChildren } from "react";
export type ThemeTokens = {
readonly scheme: "light" | "dark";
readonly accent: string;
readonly radii: { readonly card: string; readonly control: string };
};
const ThemeContext = createContext<ThemeTokens | null>(null);
export type ThemeProviderProps = PropsWithChildren<{
readonly tokens: ThemeTokens;
}>;
export function ThemeProvider({ children, tokens }: ThemeProviderProps) {
return <ThemeContext.Provider value={tokens}>{children}</ThemeContext.Provider>;
}
export function useTheme(): ThemeTokens {
const tokens = useContext(ThemeContext);
if (tokens === null) {
throw new Error("useTheme must be rendered inside a ThemeProvider");
}
return tokens;
}A consumer never mentions null, because the hook has already removed it.
// src/panel-card.tsx
import type { PropsWithChildren } from "react";
import { useTheme } from "./theme-context";
type PanelCardProps = PropsWithChildren<{
readonly heading: string;
}>;
export function PanelCard({ children, heading }: PanelCardProps) {
const theme = useTheme();
return (
<section
className={`panel panel-${theme.scheme}`}
style={{ accentColor: theme.accent, borderRadius: theme.radii.card }}
>
<h2>{heading}</h2>
<div>{children}</div>
</section>
);
}The provider is the only place the concrete value is written, so the tree states its requirement out loud.
// src/App.tsx
import type { ThemeTokens } from "./theme-context";
import { ThemeProvider } from "./theme-context";
import { PanelCard } from "./panel-card";
const darkTheme: ThemeTokens = {
scheme: "dark",
accent: "#f4b942",
radii: { card: "0.75rem", control: "0.375rem" },
};
export function App() {
return (
<ThemeProvider tokens={darkTheme}>
<PanelCard heading="Release notes">
<p>The provider owns the value type, so every consumer reads it without an assertion.</p>
</PanelCard>
</ThemeProvider>
);
}Explanation
createContext takes one type argument and one runtime argument, and both matter here. Declaring the type as ThemeTokens | null and passing null makes “no provider above me yet” a representable state instead of an impossibility the type system pretends not to see; useContext then returns exactly that union, and the hook’s null check is the point where the union collapses to ThemeTokens. Because the check throws rather than returning a fallback, the exported hook signature can promise the plain value, and the consumer in PanelCard reads theme.radii.card with no optional chaining and no assertion. PropsWithChildren is what lets the provider’s props type state the obvious contract in one place: a provider renders children and requires a token object, and forgetting either prop is a compile error rather than a silent hole in the tree.
The choice of sentinel is the part worth arguing about. A default object such as an all-light theme would type the context as plain ThemeTokens, remove the null check, and make every detached test render look correct while quietly disagreeing with production styling. undefined as the sentinel behaves the same way but reads less clearly at the check. Using a distinct value the provider could never legitimately pass means the error fires on the first render outside the provider, which is the moment you can still fix it, and the message names the hook so a stack trace points at the missing provider rather than at a property access three components away. The boundary case this creates is intentional: a subtree rendered for a unit test or a story needs a provider, and that requirement is visible in the test rather than hidden behind a default.
Two React-specific edges follow from the same typing. Reading a context happens during render, so a consumer re-renders whenever the provided value changes identity — if you build tokens inline in a parent, pass a memoised object or the whole subtree re-renders on every parent render. And the value type is one shape for all consumers: adding a fast-changing field such as the current route to ThemeTokens makes every panel card re-render with it, which is the signal to split the context into two providers rather than to widen this one.
Usage notes
- React 19 also accepts the context object itself as the provider element; the Provider property shown here stays supported, so either form works with this typing.
- Export the hook rather than the context object, and consumers cannot read the sentinel value at all.
Common mistakes
- Calling createContext with an empty object so nothing is null: the hook then returns a token set of all undefined instead of reporting the missing provider.
Caveats
- Reading a context during render subscribes the component to that context's value, so widening the value type to include a frequently changing field re-renders every consumer of the context.
- A context created with a non-null default answers correctly outside the provider, which is the behaviour this pattern deliberately refuses to give you.
Alternatives
Sharing through context suits a value several distant components read; a locally memoised derived value is the better choice when one component computes it, because context adds a provider to every tree that renders the consumer and a wider re-render surface.
Prerequisite
Prerequisite
Derive a running total during render instead of storing itDecide which value belongs in state before choosing to share it through context.
Related examples
Editorial links first, then deterministic same-task or same-topic candidates. Tags and shared language alone never qualify a candidate.
Alternative approach
Memoise a derived value with typed dependenciesReach for a memoised local value when only one component needs the result.
Same task or topic
Derive a running total during render instead of storing itSolves the Manage React state task
Same task or topic
Type a generic fetch hook that returns a known payloadShares the React State topic
References
- createContext(opens in a new tab) — React Team. Documents the returned context object and the provider plus consumer contract this example types.
- Passing Data Deeply with Context(opens in a new tab) — React Team. Shows when context replaces prop drilling and why the value type is declared once per provider.
- useContext(opens in a new tab) — React Team. Records that reading context during render subscribes the component to changes of that value.
Source page: https://devexamples.com/typescript/type-a-react-context-provider/