Typing Context
React Context is easier to use safely when the context value has a clear TypeScript type.
The main question is how to handle the initial value.
A Nullable Context
type AuthContextValue = {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextValue | null>(null);Because the default is null, consumers must handle the missing provider case.
A Custom Hook for Safety
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside AuthProvider");
}
return context;
}Now components can use useAuth() without repeating null checks.
function UserMenu() {
const { user, logout } = useAuth();
if (!user) return <a href="/login">Sign in</a>;
return <button onClick={logout}>Sign out {user.name}</button>;
}Typing the Provider
type AuthProviderProps = {
children: React.ReactNode;
};
function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
async function login(email: string, password: string) {
const nextUser = await loginRequest(email, password);
setUser(nextUser);
}
function logout() {
setUser(null);
}
const value: AuthContextValue = {
user,
login,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}Explicitly typing value helps catch missing fields.
Reducer Context
For more complex context state, use a typed reducer.
type ThemeState = {
mode: "light" | "dark";
};
type ThemeAction =
| { type: "setMode"; mode: "light" | "dark" }
| { type: "toggle" };
function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
switch (action.type) {
case "setMode":
return { mode: action.mode };
case "toggle":
return { mode: state.mode === "light" ? "dark" : "light" };
}
}The action union prevents invalid dispatches.
Splitting State and Actions
If many components only dispatch actions, consider separate contexts.
const ThemeStateContext = createContext<ThemeState | null>(null);
const ThemeDispatchContext = createContext<React.Dispatch<ThemeAction> | null>(null);This can reduce unnecessary re-renders in larger trees.
For small contexts, one combined value may be fine.
Common Mistakes
- Creating a context with
{}as a fake default value. - Using non-null assertions everywhere instead of a safe custom hook.
- Forgetting to type
children. - Putting fast-changing form state in a large app-wide context.
- Exposing loosely typed dispatch actions.
Why is a custom useAuth hook useful for a nullable context?
Practical Challenge
Create a typed ThemeContext.
Include:
mode: "light" | "dark"setMode(mode)toggleMode()- a custom
useTheme()hook that throws if used outside the provider
Then explain when you might split the state and actions into separate contexts.
Recap
Typed Context protects shared values from drifting into unclear shapes.
Prefer nullable context plus a custom hook, type provider children, and use typed reducer actions when context state has multiple transitions.