Intermediate0 questionsFull Guide

useContext Hook — Complete React Interview Guide

Master React Context for sharing state across the component tree — and learn the re-render trap that catches every developer.

The Mental Model

Imagine a radio broadcast. The radio station broadcasts on a specific frequency. Any radio within range that is tuned to that frequency receives the signal. The station doesn't need to know who is listening, and the radios don't need to be directly connected to the station. React Context works the same way. A Provider broadcasts a value on a specific "channel" (the Context object). Any component in the tree that tunes into that channel with useContext receives the current value. No matter how deeply nested the component is, it can read the context directly — no intermediate components need to pass the value down. The crucial thing to understand about Context is what it is not. Context is not state management — it doesn't manage when and how state changes. That's the job of useState or useReducer. Context is a transmission mechanism: it takes whatever value you give it and makes that value available anywhere below the Provider in the tree. There is a trap that catches almost every developer: when the context value changes, every component that calls useContext re-renders — even if the specific part of the value that component uses didn't change. This is the most important performance characteristic of Context, and it determines every architectural decision around it.

The Explanation

Creating and using Context — the three-step pattern

import { createContext, useContext, useState } from 'react'
 
// Step 1: Create the context
// The argument is the DEFAULT value — used when no Provider exists above
const ThemeContext = createContext('light')
 
// Step 2: Provide a value to the subtree
function App() {
  const [theme, setTheme] = useState('light')
 
  return (
    // Any component inside this Provider can read theme/setTheme
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Navbar />
      <Main />
      <Footer />
    </ThemeContext.Provider>
  )
}
 
// Step 3: Consume anywhere in the subtree — no prop drilling
function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext)
 
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  )
}
// ThemeToggle can be nested 10 levels deep — still reads theme directly

The default value — when is it used?

const UserContext = createContext(null)
// null is the default — used when component renders OUTSIDE any Provider
 
function ProfileAvatar() {
  const user = useContext(UserContext)
  // If rendered without UserContext.Provider above it, user is null (the default)
  return user ? <img src={user.avatar} /> : <GuestIcon />
}
 
// Inside app — Provider exists
<UserContext.Provider value={currentUser}>
  <ProfileAvatar />  {/* user = currentUser */}
</UserContext.Provider>
 
// In Storybook / unit test — no Provider
<ProfileAvatar />  {/* user = null (default) — no crash */}

The re-render trap — every consumer re-renders

This is the most important thing to understand about Context performance:

const AppContext = createContext(null)
 
function App() {
  const [user, setUser]         = useState(null)
  const [theme, setTheme]       = useState('light')
  const [notifications, setN]   = useState([])
 
  // ✗ One context for everything
  return (
    <AppContext.Provider value={{ user, theme, notifications, setUser, setTheme, setN }}>
      <Navbar />  {/* re-renders when ANYTHING in value changes */}
      <Main />    {/* re-renders when ANYTHING in value changes */}
    </AppContext.Provider>
  )
}
 
// Navbar only uses theme — but re-renders when user changes or notifications change!
function Navbar() {
  const { theme } = useContext(AppContext)  // reads one field
  // Re-renders when user changes or notifications change — wasteful!
  return <nav className={theme}>...</nav>
}

Solution 1 — Split contexts by update frequency

// Separate contexts — each only triggers its own consumers
const UserContext         = createContext(null)   // changes on login/logout
const ThemeContext         = createContext('light') // changes on toggle
const NotificationContext  = createContext([])      // changes frequently
 
function App() {
  const [user, setUser]         = useState(null)
  const [theme, setTheme]       = useState('light')
  const [notifications, setN]   = useState([])
 
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <NotificationContext.Provider value={{ notifications, setN }}>
          <AppContent />
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  )
}
 
// Navbar only re-renders when theme changes — not when user or notifications change
function Navbar() {
  const { theme } = useContext(ThemeContext)  // isolated
  return <nav className={theme}>...</nav>
}

Solution 2 — Separate state from dispatch

Since useReducer's dispatch function is always stable, putting it in a separate context means "dispatch-only" components never re-render when state changes:

const StateContext    = createContext(null)  // changes on every action
const DispatchContext = createContext(null)  // NEVER changes — dispatch is stable
 
function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState)
 
  return (
    <DispatchContext.Provider value={dispatch}>   {/* stable forever */}
      <StateContext.Provider value={state}>         {/* changes on updates */}
        {children}
      </StateContext.Provider>
    </DispatchContext.Provider>
  )
}
 
// Button that only dispatches — NEVER re-renders due to state changes
function AddButton() {
  const dispatch = useContext(DispatchContext)  // stable reference
  return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>
}
 
// Display that reads state — re-renders when state changes (expected)
function Counter() {
  const { count } = useContext(StateContext)
  return <p>Count: {count}</p>
}

Custom hook wrapper — safer context consumption

// Expose context through a custom hook with runtime error checking
const AuthContext = createContext(null)
 
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const login  = useCallback(async (creds) => { /* ... */ }, [])
  const logout = useCallback(async () => { setUser(null) }, [])
 
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}
 
export function useAuth() {
  const ctx = useContext(AuthContext)
  if (!ctx) throw new Error('useAuth must be inside AuthProvider')
  return ctx
}
 
// Usage — clear error if Provider is missing instead of silent null crash
function ProfilePage() {
  const { user } = useAuth()  // throws immediately if no AuthProvider above
  return <div>{user.name}</div>
}

Context vs prop drilling vs state management

ApproachBest forAvoid when
Props1-2 levels, explicit data flow3+ levels, many consumers
Composition (children)Avoiding drilling without contextCan't restructure hierarchy
ContextTheme, auth, locale — infrequently changingHigh-frequency updates
Zustand / ReduxComplex state, frequent updates, DevToolsSimple local state

Common Misconceptions

⚠️

Many developers think Context is state management — but Context is a transmission mechanism, not state management. It broadcasts a value; it doesn't decide when that value changes. useState and useReducer manage the state. Context distributes it. Conflating the two leads to architectural mistakes like putting all application state in a single context object.

⚠️

Many developers think components only re-render when the part of context they use changes — but any component calling useContext re-renders whenever the Provider's value prop reference changes, regardless of which fields the component reads. A component reading only user.name still re-renders when theme changes, if they're in the same context.

⚠️

Many developers think the default value in createContext is a good fallback for missing Providers — using null as a default and checking in a custom hook is better because it throws an explicit error when a component is used outside its Provider, rather than silently using a potentially incorrect default value.

⚠️

Many developers think nested Providers always create isolated scopes — Providers of the same context CAN be nested, and each acts as a new source of that context for its subtree. The nearest Provider above a consumer wins. This can be used intentionally for theme overrides or test mocking, but accidental nesting can cause confusion.

⚠️

Many developers think Context is the right solution for all global state — Context re-renders all consumers on every value change. For state that updates frequently (every keystroke, every WebSocket message, every animation frame), use Zustand or Jotai which have built-in subscription optimization that Context lacks.

Where You'll See This in Real Code

Authentication state is the canonical Context use case. Auth context provides user, login, logout, and loading — used across the entire app tree in navigation, protected routes, and profile sections. It changes rarely (login/logout events) making it ideal for Context without performance concerns.

Theme systems at companies like CRED and PhonePe use multiple nested Theme Contexts: a global theme at the app root and local theme overrides at the section level. A dark-mode card inside a light-mode page is achieved by wrapping the card in a ThemeContext.Provider with the dark values — the nearest provider wins.

Internationalization (i18n) libraries like react-i18next use Context to provide the translation function (t) to all components. Since the locale rarely changes, Context re-renders are acceptable. Components call useContext to get t() and render translated strings without knowing which language is active.

Toast notification systems typically use Context to provide an addToast function to all components. A component deep in the tree can trigger a toast without prop drilling by calling const { toast } = useToast() — the toast manager is at the root, dispatching creates the notification, and the Context provides the dispatch function everywhere.

Multi-step forms use a FormContext to share form state and dispatch across step components without lifting all state to a parent that doesn't otherwise need it. Each step reads and updates the relevant slice of form state through the context.

Interview Cheat Sheet

  • createContext(defaultValue) creates a Context — default is used when no Provider exists above
  • Context.Provider value={...} broadcasts the value to all consumers in its subtree
  • useContext(MyContext) subscribes a component to the context — re-renders when value changes
  • Every consumer re-renders when Provider's value prop reference changes — regardless of which fields they read
  • Split contexts by update frequency to avoid unnecessary re-renders
  • Separate state context from dispatch context — dispatch is always stable, its consumers never re-render
  • Default null + custom hook that throws = better DX than silent null bugs
  • Nearest Provider wins — context can be nested, inner Provider overrides outer for its subtree
  • Context is NOT state management — useState/useReducer manage state, Context distributes it
  • When to use Context: theme, auth, locale, feature flags — things that change infrequently
  • When to use Zustand/Redux instead: cart, search, real-time data — things that change frequently
💡

How to Answer in an Interview

  • 1.When asked "what is the difference between Context and Redux/Zustand", lead with the re-render behavior: "Context re-renders all consumers on every value change. Zustand and Redux have subscription optimisation — components only re-render when the specific slice they subscribe to changes." This immediately shows you understand the architectural trade-off, not just the syntax.
  • 2.The context re-render trap is asked as an output prediction question at Atlassian and Razorpay: "Does component X re-render when Y changes in the same context?" The answer is always yes if X calls useContext — it doesn't matter which fields it reads. The solution is splitting contexts.
  • 3.When describing your own architecture decisions: "For theme and auth I use Context — they change once or twice per session. For the cart and search results I use Zustand — they update on every user interaction. Using Context for those would re-render every component that reads any part of the context on every keystroke."
  • 4.The separate state/dispatch context pattern (useReducer + two contexts) shows deep knowledge of how React works. The key insight to explain: "useReducer's dispatch function never changes — it's the same reference for the entire lifetime of the component. So I put it in its own context. Components that only dispatch (buttons, forms) never re-render when state changes."
  • 5.Always mention the custom hook wrapper pattern in interviews: "I never expose useContext directly — I always wrap it in a custom hook that throws if used outside the Provider. This turns a silent undefined bug at runtime into a clear error message during development." This shows production-level thinking.

Practice Questions

No questions tagged to this topic yet.

Related Topics

useState Hook — Complete React Interview Guide
Beginner·4–8 Qs
useCallback Hook — Complete React Interview Guide
Intermediate·4–8 Qs
useMemo Hook — Complete React Interview Guide
Intermediate·4–8 Qs
useReducer Hook — Complete React Interview Guide
Intermediate·4–8 Qs
useEffect Hook — Complete React Interview Guide
Beginner·4–8 Qs
useRef Hook — Complete React Interview Guide
Beginner·4–8 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz