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
useContextwithout 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.
createContextcreates the context object;Providersupplies the value;useContextreads it.- The default value passed to
createContextonly activates when no Provider exists above in the tree.
Quick example
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
useReducerhandles 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
// 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
// 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 unexpectedlyReact 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
// 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
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:
SessionProviderwraps the entire app and exposes session data. Any page reads it viauseSession(), which internally uses context. - Chakra UI / shadcn/ui:
ColorModeProviderdistributes theme tokens. Components read the active mode without props. - React Router:
RouterContextunder the hood. EveryuseParams()anduseNavigate()call reads it. - Vercel dashboard (public architecture talks):
UserContextfor 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
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
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
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 readyA concise answer to help you respond confidently on this topic during an interview.