Skip to content

TypeScript React Context — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about TypeScript React Context. We cover key concepts, practical examples, and best practices to help you master this topic.

React Context with TypeScript provides type-safe global state management without prop drilling — typed context values, providers, and context consumers ensure that every component accessing shared state does so with full type safety.

What You'll Learn

  • Typed createContext and useContext
  • Custom provider components
  • Context with useReducer
  • Context composition patterns

Why It Matters

Without TypeScript, context values are any by default — you lose all autocompletion and safety. Typed context guarantees that every component consuming the context accesses valid properties with correct types.

Real-World Use

DodaTech's Durga Antivirus Pro dashboard uses typed React Context for the current user session, theme preferences, and scan state. The typed ScanContext ensures every component accessing scan status uses the correct action types and state shape.

Learning Path

flowchart LR
  A[React Events] --> B[React Context]
  B --> C[State Management]
  B --> D[You Are Here]
  C --> E[React Advanced]
  E --> F[Node Setup]

Basic Typed Context

import { createContext, useContext, ReactNode } from 'react';

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// Usage
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button
      onClick={toggleTheme}
      style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}
    >
      Toggle Theme
    </button>
  );
}

Context with useReducer

interface ScanState {
  status: 'idle' | 'scanning' | 'complete' | 'error';
  threats: string[];
  progress: number;
}

type ScanAction =
  | { type: 'START' }
  | { type: 'PROGRESS'; progress: number }
  | { type: 'THREAT'; name: string }
  | { type: 'COMPLETE' }
  | { type: 'ERROR'; message: string };

function scanReducer(state: ScanState, action: ScanAction): ScanState {
  switch (action.type) {
    case 'START': return { status: 'scanning', threats: [], progress: 0 };
    case 'PROGRESS': return { ...state, progress: action.progress };
    case 'THREAT': return { ...state, threats: [...state.threats, action.name] };
    case 'COMPLETE': return { ...state, status: 'complete', progress: 100 };
    case 'ERROR': return { ...state, status: 'error' };
  }
}

interface ScanContextType {
  state: ScanState;
  dispatch: React.Dispatch<ScanAction>;
}

const ScanContext = createContext<ScanContextType | undefined>(undefined);

function ScanProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(scanReducer, {
    status: 'idle', threats: [], progress: 0,
  });

  return (
    <ScanContext.Provider value={{ state, dispatch }}>
      {children}
    </ScanContext.Provider>
  );
}

function useScan(): ScanContextType {
  const context = useContext(ScanContext);
  if (!context) throw new Error('useScan must be used within ScanProvider');
  return context;
}

// Components
function ScanButton() {
  const { dispatch } = useScan();
  return <button onClick={() => dispatch({ type: 'START' })}>Start Scan</button>;
}

function ProgressBar() {
  const { state } = useScan();
  if (state.status === 'idle') return <p>Ready</p>;
  return <progress value={state.progress} max={100} />;
}

Composing Multiple Contexts

function AppProviders({ children }: { children: ReactNode }) {
  return (
    <ThemeProvider>
      <ScanProvider>
        <AuthProvider>
          {children}
        </AuthProvider>
      </ScanProvider>
    </ThemeProvider>
  );
}

// Custom hook that combines contexts
function useAppState() {
  const theme = useTheme();
  const scan = useScan();
  const auth = useAuth();
  return { theme, scan, auth };
}

Context Factory Patternory" >}} Pattern

function createCtx<T>(name: string) {
  const ctx = createContext<T | undefined>(undefined);

  function useCtx(): T {
    const c = useContext(ctx);
    if (!c) throw new Error(`use${name} must be inside ${name}Provider`);
    return c;
  }

  return [ctx, useCtx] as const;
}

// Usage
interface UserContextType {
  user: { id: string; name: string } | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const [UserContext, useUser] = createCtx<UserContextType>('User');
// UserContext — the raw context object
// useUser — typed hook with error handling

Avoiding Unnecessary Re-renders

function SplitContexts() {
  // Split reading and writing contexts to avoid re-renders
  const ThemeValueContext = createContext<ThemeState>({ theme: 'light' });
  const ThemeActionsContext = createContext<ThemeActions>({ toggleTheme: () => {} });

  // Components that only read don't re-render when dispatch changes
  // Components that only dispatch don't re-render when state changes
}

Common Mistakes

1. Not Handling Undefined Context

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

2. Default Value at createContext That Makes Sense

// Bad: default value is nonsense
createContext<ThemeContextType>({} as ThemeContextType);

// Good: undefined with runtime check
createContext<ThemeContextType | undefined>(undefined);

3. Putting Everything in One Context

Split contexts by concern — auth state, theme, scan status — to prevent unnecessary re-renders.

4. Not Memoizing Context Values

// Bad: new object every render
<MyContext.Provider value={{ user, login }}>
  {children}
</MyContext.Provider>

// Good: memoized
const value = useMemo(() => ({ user, login }), [user, login]);
<MyContext.Provider value={value}>{children}</MyContext.Provider>

5. Forgetting to Export the Hook

Always export the typed consumer hook from your context module.

Practice Questions

  1. Why should context value include undefined in the type? So the hook can throw a meaningful error when used outside the provider.

  2. How do you prevent unnecessary re-renders with context? Split into value/actions contexts, memoize the value, or use context selectors.

  3. What is the context factory pattern? A function that creates both the context and a typed consumer hook, reducing boilerplate.

  4. How do you combine multiple providers? Nest them, create an AppProviders wrapper, and optionally create a combined hook.

Challenge: Create a typed NotificationContext that manages a list of toast notifications with addNotification(type, message) and removeNotification(id). Provide type-safe hooks.

FAQ

What is the difference between context and prop drilling?

Prop drilling passes data through intermediate components that don't use it. Context skips them, providing data directly to consumers.

Can I have multiple contexts?

Yes. Composition is preferred — each context handles a specific concern.

Does context cause performance issues?

Yes, if not used carefully. Context consumers re-render on any value change. Split contexts by change frequency.

What is the default value in createContext for?

Used when a component consumes the context outside a provider. Usually undefined with a runtime check.

How do I type context without a default value?

createContext<MyType | undefined>(undefined) and check in the consumer hook.

Mini Project: Auth Context

interface AuthState { user: User | null; token: string | null; }
interface AuthActions {
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

type AuthContextType = AuthState & AuthActions;

const AuthContext = createContext<AuthContextType | undefined>(undefined);

function AuthProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<AuthState>({ user: null, token: null });

  const login = async (email: string, password: string) => {
    const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) });
    const data = await res.json();
    setState({ user: data.user, token: data.token });
  };

  const logout = () => setState({ user: null, token: null });

  const value = useMemo(() => ({ ...state, login, logout }), [state]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

function useAuth(): AuthContextType {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}

What's Next

Now explore state management libraries:

Lesson Description
{{< ref "/programming-languages/typescript/39-react-events" >}} Review events
{{< ref "/programming-languages/typescript/41-react-state-management" >}} Zustand, Redux Toolkit, Jotai
{{< ref "/programming-languages/typescript/42-react-advanced" >}} forwardRef, HOCs, render props

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro