How do you type context?
1) The basic, safe pattern (with undefined + a custom hook)
When the context is required inside a subtree and must not be null/undefined when it is used.
javascript
import React, { createContext, useContext, ReactNode } from "react";
type Theme = "light" | "dark";
type ThemeCtx = {
theme: Theme;
setTheme: (t: Theme) => void;
};
// 1) The context allows undefined before the Provider
const ThemeContext = createContext<ThemeCtx | undefined>(undefined);
// 2) A safe hook: throws a clear error if there is no provider
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx;
}
// 3) A provider with typed props
export function ThemeProvider({
value,
children,
}: {
value: ThemeCtx;
children: ReactNode;
}) {
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Why this is better:
- there is no need to write
!or cast withas ThemeCtx; - you get an early, clear error if you forgot to wrap things in the provider.
2) Context with a valid default (if one genuinely exists)
If the value makes sense outside the provider (for example, read-only settings).
javascript
type Config = { apiBase: string; locale: string };
const defaultConfig: Config = { apiBase: "/api", locale: "en" };
const ConfigContext = createContext<Config>(defaultConfig);
export const useConfig = () => useContext(ConfigContext);Important: only give it a default when it is genuinely correct at runtime, otherwise you will hide a bug.
3) Context that can be null by design
If "no value" is a normal case for the domain.
javascript
type User = { id: string; name: string } | null;
const CurrentUserContext = createContext<User>(null);
export const useCurrentUser = () => useContext(CurrentUserContext);4) A "state + dispatch" pair (handy for reducers)
javascript
type Action =
| { type: "set"; payload: number }
| { type: "reset" };
type CounterState = { count: number };
type CounterCtx = {
state: CounterState;
dispatch: React.Dispatch<Action>;
};
const CounterContext = createContext<CounterCtx | undefined>(undefined);
export function useCounter() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error("useCounter must be used within <CounterProvider>");
return ctx;
}5) A tuple/multi-value (sometimes more convenient)
javascript
type Filters = { q: string; tags: string[] };
type SetFilters = (next: Filters) => void;
const FiltersContext = createContext<[Filters, SetFilters] | undefined>(undefined);
export function useFilters() {
const ctx = useContext(FiltersContext);
if (!ctx) throw new Error("useFilters must be used within <FiltersProvider>");
return ctx; // returns [filters, setFilters]
}6) Typing the provider (the value and children props)
javascript
type AuthCtx = { token: string | null; login: (t: string) => void };
const AuthContext = createContext<AuthCtx | undefined>(undefined);
type AuthProviderProps = {
value: AuthCtx; // strictly type value
children: ReactNode;
};
export function AuthProvider({ value, children }: AuthProviderProps) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}7) Avoid anti-patterns
createContext<Ctx>(null as unknown as Ctx)ornull!- it hides errors.- Giving a "fake" default when the app genuinely cannot work without the provider.
- Storing frequently changing primitives in context without a reason (it causes unnecessary re-renders).
8) Nuances and tips
-
For large values, memoize
valuein the provider:javascriptconst value = useMemo(() => ({ theme, setTheme }), [theme]); -
For SSR/tests, it is convenient to have a valid default (pattern #2), if it is genuinely safe.
-
If the context is used across different packages, export both the context and the hook, but hand consumers the hook specifically - that way you enforce safe usage.
A short cheat sheet
- Provider is required ->
createContext<T | undefined>(undefined)+useXxx()that throws. - A meaningful default exists ->
createContext<T>(defaultValue). - Absence is allowed ->
createContext<T | null>(null). - Reducer style ->
createContext<{ state: S; dispatch: Dispatch<A> } | undefined>(undefined).
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.