//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Create fully typed React contexts with createContext, typed providers, and type-safe useContext hooks. Handle the default value problem cleanly.
// 1. Define the context type
type Theme = "light" | "dark";
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
// 2. Create context with a sensible default or null
const ThemeContext = createContext<ThemeContextType | null>(null);
// 3. Create a typed hook with a runtime guard
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
// 4. Create the provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 5. Consume in a component
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>Current: {theme}</button>
</header>
);
}createContext<T>(defaultValue) creates a context object. The generic T defines the shape of the value that providers must supply and consumers will receive.null default + custom hook pattern avoids two problems: (1) inventing a fake default value that could mask bugs, and (2) forcing consumers to check for undefined everywhere.useTheme() hook narrows the type from ThemeContextType | null to ThemeContextType by throwing if the context is missing. This gives consumers a clean, non-nullable type.value changes. Since { theme, toggleTheme } is a new object on every render, wrap it in useMemo if you have many consumers.Context with useMemo for stable value:
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const value = useMemo<ThemeContextType>(
() => ({
theme,
toggleTheme: () => setTheme((prev) => (prev === "light" ? "dark" : "light")),
}),
[theme]
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Multiple contexts for separate concerns:
type AuthContextType = {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within AuthProvider");
return context;
}Context with useReducer:
type AppState = { count: number; user: User | null };
type AppAction = { type: "increment" } | { type: "setUser"; payload: User };
type AppContextType = {
state: AppState;
dispatch: React.Dispatch<AppAction>;
};
const AppContext = createContext<AppContextType | null>(null);React.Dispatch<React.SetStateAction<T>> is the type of a useState setter. Use it in context types when exposing a setter directly.React.Dispatch<Action> is the type of a useReducer dispatch function.as to cast the default value: createContext({} as ThemeContextType) compiles but provides an invalid object at runtime if the provider is missing.createContext({} as T) silences TypeScript but leads to runtime crashes when the provider is absent. The null + guard pattern is safer.value on every render causes all consumers to re-render. Use useMemo for the value object.useTheme, useAuth) is critical for good DX. Without it, consumers must import both the context and useContext, and handle null themselves.| Approach | Pros | Cons |
|---|---|---|
null default + guard hook | Type-safe, clear error on misuse | Requires custom hook per context |
Non-null assertion ({} as T) | No null checks needed | Runtime crash if provider missing |
| Real default value | Works without provider | Must invent a meaningful default |
| Zustand or Jotai | Simpler API, fine-grained subscriptions | External dependency |
| Module-level state | No provider nesting | Not reactive, not SSR-safe |
createContext({} as T) provides an invalid object that silently causes runtime crashes if the provider is missing.null default combined with a guard hook gives a clear error message when the provider is absent.function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context; // narrowed to ThemeContextType
}T | null to T with a single runtime guard.value reference changes.{ theme, toggleTheme } creates a new object on every render, triggering re-renders.useMemo to produce a stable reference.AuthContext, ThemeContext) prevent unnecessary re-renders.React.Dispatch<React.SetStateAction<T>>.setState(5) and setState(prev => prev + 1).createContext({} as ThemeContextType) compiles but provides an object with no real properties.null + guard pattern is strictly safer.type AppContextType = {
state: AppState;
dispatch: React.Dispatch<AppAction>;
};
const AppContext = createContext<AppContextType | null>(null);React.Dispatch<Action> is the type of a useReducer dispatch function.useMemo becomes excessive.const value = useMemo<ThemeContextType>(
() => ({
theme,
toggleTheme: () => setTheme((p) => (p === "light" ? "dark" : "light")),
}),
[theme]
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;useMemo ensures the value object only changes when dependencies change.Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥