Skip to main content

What is context and useContext hook in React

React Context is a built-in way to share data across the component tree without passing props through every intermediate level. useContext is the Hook that reads that shared data inside any function component.

Theory

TL;DR

  • Context is like a company bulletin board: post the value once at the top (Provider), and any component grabs it with useContext without bothering every floor in between.
  • The main point: Context removes prop drilling. Components subscribe and re-render automatically when the value changes.
  • Use it when 3 or more components need the same data. For 1-2 levels, plain props are simpler.
  • createContext creates the context object; Provider supplies the value; useContext reads it.
  • The default value passed to createContext only activates when no Provider exists above in the tree.

Quick example

jsx
import { createContext, useContext } from 'react'; // Create once, outside any component const ThemeContext = createContext('light'); // 'light' is the fallback default function App() { return ( // Provider sets the value for the entire subtree <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar() { return <div><Button /></div>; // Receives no props, forwards nothing } function Button() { const theme = useContext(ThemeContext); // Reads 'dark' directly return <button className={theme}>I am {theme}</button>; }

Button reads "dark" directly from context. Toolbar never touches the value. No prop chain needed.

Key difference from props

Props are explicit handoffs between parent and child, one level at a time. Every component in the chain must forward the value even if it does not use it. Context removes that chain. The Provider puts a value on a shared channel, and any descendant reads it directly via useContext, no matter how deep it sits. The components in between stay clean.

When to use

  • Same data needed in many components (user auth, theme, locale): use Context.
  • Data passes only 1-2 levels deep: use props. More explicit, easier to trace.
  • Frequent updates across the tree: Context paired with useReducer handles this well.
  • Complex global state with many actions: consider Redux or Zustand. Context has no built-in devtools, middleware, or selector support.

How React finds the value internally

When you call useContext(ThemeContext), React walks up the fiber tree from the calling component and finds the nearest ThemeContext.Provider. The result is cached per context type in that component's fiber node. In React 18, when the Provider value changes, React schedules re-renders only for subscribed consumers via a dedicated dispatcher queue, not by traversing the full tree each time. Components that never called useContext for that context are not touched.

Common mistakes

Mistake 1: Creating context inside a component

jsx
// Wrong: new context object on every render function App() { const MyContext = createContext(); // Breaks memoization for all consumers return <MyContext.Provider value={data}>...</MyContext.Provider>; } // Correct: declare outside the component const MyContext = createContext(); function App() { return <MyContext.Provider value={data}>...</MyContext.Provider>; }

Each render produces a new context reference, which forces all subscribers to re-render even when nothing changed.

Mistake 2: Mutating the context value directly

jsx
// Wrong: mutating the shared object reference const config = { theme: 'light' }; <Provider value={config}>...</Provider> // Somewhere in a child: config.theme = 'dark'; // Other consumers see this unexpectedly

React expects new object references to trigger re-renders. Mutation bypasses that, so updates become unpredictable. Pass a setter function instead: value={{ theme, setTheme }}.

Mistake 3: Wrong Provider nesting order with multiple contexts

jsx
// Mixed-up order: useContext(AuthContext) inside may get the wrong value <ThemeProvider> <AuthProvider> <App /> {/* useContext(AuthContext) resolves correctly here */} </AuthProvider> </ThemeProvider>

useContext finds the nearest matching Provider by walking up. If nesting is wrong, a component silently falls back to the default value instead of the real one. That bug took me 30 minutes to track down the first time, because everything else looked correct.

Mistake 4: Using Context for high-frequency updates

Context is not a state manager. Putting a frequently changing value (like a text input's current value) in Context re-renders every subscriber on each keystroke. Local state or a dedicated store is the right tool for that.

Mistake 5: No default value on createContext

jsx
const AuthContext = createContext(); // undefined by default // A component rendered outside any Provider: function Orphan() { const { user } = useContext(AuthContext); // TypeError: cannot destructure undefined }

Always provide a meaningful default, or at least createContext(null) and guard against it in consumers.

Real-world usage

  • Next.js / next-auth: SessionProvider wraps the entire app and exposes session data. Any page reads it via useSession(), which internally uses context.
  • Chakra UI / shadcn/ui: ColorModeProvider distributes theme tokens. Components read the active mode without props.
  • React Router: RouterContext under the hood. Every useParams() and useNavigate() call reads it.
  • Vercel dashboard (public architecture talks): UserContext for org switching between the nav and the sidebar.

Follow-up questions

Q: What does useContext return when the component is outside any Provider?
A: The default value passed to createContext. No error, no crash. This is why a meaningful default matters more than it seems.

Q: Does every context value change re-render all consumers?
A: Yes, by default. If the Provider receives a new object reference on every parent render (like value={{ user }}), all subscribers re-render. Wrap the value in useMemo to stabilize the reference.

Q: How is useContext different from the older Context.Consumer pattern?
A: Consumer uses a render prop and works in both class and function components. useContext is a Hook, so it only works in function components. The code ends up much cleaner.

Q: When should you combine Context with useReducer?
A: When shared state has multiple update cases (login, logout, role change). useReducer keeps the logic organized. You expose { state, dispatch } as the context value. Dan Abramov described this as the right way to scale Context without reaching for an external store.

Q: Why do large apps often prefer Redux or Zustand over raw Context?
A: Context has no devtools, no middleware, and no selector support. You cannot subscribe to only part of the state, so any value change re-renders every consumer. Once you have 10+ actions and need time-travel debugging, a dedicated store is the better trade-off.

Q: How do React 18 transitions interact with context updates?
A: Wrap the Provider value change in startTransition to mark it as non-urgent. React defers that re-render pass, keeping the UI responsive during heavy updates.

Examples

Basic: theme Provider

jsx
import { createContext, useContext } from 'react'; const ThemeContext = createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Page /> </ThemeContext.Provider> ); } function Page() { return <Header />; // Receives nothing, passes nothing } function Header() { const theme = useContext(ThemeContext); // 'dark' return <header className={theme}>Site header</header>; }

Page has no idea theme exists. Header reads it directly. This is the pattern in shadcn/ui and Chakra UI for design tokens.

Intermediate: auth context with a login action

jsx
import { createContext, useContext, useState } from 'react'; const AuthContext = createContext(null); export function AuthProvider({ children }) { const [user, setUser] = useState({ name: 'Alex', id: 1 }); return ( <AuthContext.Provider value={{ user, login: setUser }}> {children} </AuthContext.Provider> ); } function Profile() { const { user, login } = useContext(AuthContext); return ( <div> <p>Welcome, {user.name}!</p> {/* Shows 'Alex' */} <button onClick={() => login({ name: 'Bob', id: 2 })}> Switch user </button> </div> ); } function App() { return ( <AuthProvider> <Profile /> </AuthProvider> ); }

Click the button and only Profile re-renders. Components that never called useContext(AuthContext) are not affected. This mirrors the next-auth SessionProvider pattern used in production Next.js apps.

Advanced: default value fallback and nested Providers

jsx
const ThemeContext = createContext('light'); // Active when no Provider is above function App() { return ( <ThemeContext.Provider value="dark"> <Nested /> </ThemeContext.Provider> ); } function Nested() { return <DeepChild />; } function DeepChild() { const theme = useContext(ThemeContext); // 'dark' from App's Provider return <span>{theme}</span>; } // Rendered outside the Provider tree: function Orphan() { const theme = useContext(ThemeContext); // 'light' (default), no crash return <span>{theme}</span>; }

DeepChild gets "dark". Orphan gets "light". React does not throw when a Provider is missing. It falls back to the default silently, which is why the default value choice matters for graceful degradation.

Short Answer

Interview ready
Premium

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

Finished reading?