Context solves the "prop drilling" problem — passing the same value through 5 layers of components just to reach one deep child. Here's how to use it correctly, and when not to.

The Problem Context Solves

// Without Context — theme prop drilled through every layer
function App() {
  const [theme, setTheme] = useState('light')
  return <Layout theme={theme} setTheme={setTheme} />
}

function Layout({ theme, setTheme }: any) {
  return <Sidebar theme={theme} setTheme={setTheme} />
}

function Sidebar({ theme, setTheme }: any) {
  return <ThemeToggle theme={theme} setTheme={setTheme} />
}

// ThemeToggle finally uses it — after drilling through 3 unnecessary layers
function ThemeToggle({ theme, setTheme }: any) {
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>
}

With Context, ThemeToggle reads the value directly, no drilling needed.

Creating a Context

// src/contexts/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'

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

// 1. Create context with a default value (used if no Provider wraps the component)
const ThemeContext = createContext<ThemeContextType>({
  theme: 'light',
  toggleTheme: () => {}
})

// 2. Create provider component
export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')

  function toggleTheme() {
    setTheme(t => t === 'light' ? 'dark' : 'light')
    document.documentElement.setAttribute('data-bs-theme', theme === 'light' ? 'dark' : 'light')
  }

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

// 3. Custom hook — cleaner than calling useContext directly everywhere
export function useTheme() {
  return useContext(ThemeContext)
}
// src/main.tsx — wrap the app with the provider
import { ThemeProvider } from './contexts/ThemeContext'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <ThemeProvider>
    <App />
  </ThemeProvider>
)
// Any component, any depth — reads theme directly
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme()

  return (
    <button
      onClick={toggleTheme}
      className={`btn btn-sm ${theme === 'dark' ? 'btn-light' : 'btn-dark'}`}
    >
      {theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
    </button>
  )
}

Auth Context — Real-World Pattern

// src/contexts/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'

interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

interface AuthContextType {
  user:    User | null
  loading: boolean
  login:   (email: string, password: string) => Promise<void>
  logout:  () => void
  isAdmin: boolean
}

const AuthContext = createContext<AuthContextType | null>(null)

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user,    setUser]    = useState<User | null>(null)
  const [loading, setLoading] = useState(false)

  async function login(email: string, password: string) {
    setLoading(true)
    try {
      const res  = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
        headers: { 'Content-Type': 'application/json' }
      })
      const data = await res.json()
      setUser(data.user)
      localStorage.setItem('token', data.token)
    } finally {
      setLoading(false)
    }
  }

  function logout() {
    setUser(null)
    localStorage.removeItem('token')
  }

  return (
    <AuthContext.Provider value={{
      user,
      loading,
      login,
      logout,
      isAdmin: user?.role === 'admin'
    }}>
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const ctx = useContext(AuthContext)
  if (!ctx) throw new Error('useAuth must be used inside AuthProvider')
  return ctx
}
// Usage — anywhere in the tree
function Navbar() {
  const { user, logout, isAdmin } = useAuth()

  return (
    <nav className="navbar navbar-expand-lg bg-dark navbar-dark px-4">
      <a className="navbar-brand fw-bold" href="/">LettStart</a>
      <div className="ms-auto d-flex align-items-center gap-3">
        {isAdmin && <span className="badge" style={{ background: '#fd4766' }}>Admin</span>}
        {user ? (
          <>
            <span className="text-white small">{user.name}</span>
            <button className="btn btn-sm btn-outline-light" onClick={logout}>Sign Out</button>
          </>
        ) : (
          <a href="/login" className="btn btn-sm text-white" style={{ background: '#fd4766' }}>Sign In</a>
        )}
      </div>
    </nav>
  )
}

Context + useReducer — For Complex State

When a context value has many related pieces of state, useReducer makes updates more predictable:

// src/contexts/CartContext.tsx
import { createContext, useContext, useReducer, ReactNode } from 'react'

interface CartItem { id: number; name: string; price: number; qty: number }
interface CartState { items: CartItem[]; open: boolean }
type CartAction =
  | { type: 'ADD';    item: Omit<CartItem, 'qty'> }
  | { type: 'REMOVE'; id: number }
  | { type: 'UPDATE'; id: number; qty: number }
  | { type: 'CLEAR' }
  | { type: 'TOGGLE' }

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD': {
      const exists = state.items.find(i => i.id === action.item.id)
      return {
        ...state,
        items: exists
          ? state.items.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i)
          : [...state.items, { ...action.item, qty: 1 }]
      }
    }
    case 'REMOVE': return { ...state, items: state.items.filter(i => i.id !== action.id) }
    case 'UPDATE': return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, qty: action.qty } : i) }
    case 'CLEAR':  return { ...state, items: [] }
    case 'TOGGLE': return { ...state, open: !state.open }
    default:       return state
  }
}

const CartContext = createContext<{ state: CartState; dispatch: React.Dispatch<CartAction> } | null>(null)

export function CartProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [], open: false })
  return <CartContext.Provider value={{ state, dispatch }}>{children}</CartContext.Provider>
}

export function useCart() {
  const ctx = useContext(CartContext)
  if (!ctx) throw new Error('useCart must be used inside CartProvider')

  const { state, dispatch } = ctx
  return {
    items:    state.items,
    open:     state.open,
    total:    state.items.reduce((s, i) => s + i.price * i.qty, 0),
    count:    state.items.reduce((s, i) => s + i.qty, 0),
    addItem:  (item: Omit<CartItem, 'qty'>) => dispatch({ type: 'ADD', item }),
    removeItem: (id: number) => dispatch({ type: 'REMOVE', id }),
    clear:    () => dispatch({ type: 'CLEAR' }),
    toggle:   () => dispatch({ type: 'TOGGLE' }),
  }
}

When Context Is the Wrong Tool

Context re-renders every component that calls useContext() whenever the context value changes — even if that component only uses a slice of the value that didn't change.

SituationBetter tool
Theme, locale, current userContext ✅
Shopping cart (frequent updates)Zustand or Redux Toolkit
Server data (templates, users list)React Query / TanStack Query
URL-derived stateURL search params
Form stateReact Hook Form
Sibling component communicationLift state up to parent

If you're running into performance issues with Context, look at Zustand first — it's 1KB, zero boilerplate, and only re-renders components that subscribe to the specific slice of state they use.

Frequently Asked Questions

React Context lets you share values across a component tree without passing props through every level. It's best for data that many components at different nesting levels need — current user, theme, language, auth status. It's not a full state management solution like Redux.
Use Context for slow-changing global values like theme, locale, or the current user. Use Zustand or Redux Toolkit for frequently-updated shared state (shopping cart, real-time data, complex forms) since Context re-renders every consumer on every update, which hurts performance.
Split contexts by update frequency — put rarely-changing values (user, theme) in one context, frequently-changing values (cart count, notifications) in another. Memoize the context value with useMemo. Move context providers as close to where they're needed as possible rather than wrapping the entire app.
Yes. Call useContext multiple times: const { user } = useContext(AuthContext); const { theme } = useContext(ThemeContext). There's no limit. This is actually preferred over one giant context — smaller contexts re-render fewer consumers.

Need a Full Bootstrap 5 Admin Dashboard?

Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off your first purchase.

Related Components