Skip to main content

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 with as 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) or null! - 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 value in the provider:

    javascript
    const 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.