Master the React Context API — how to avoid prop drilling, when context re-renders every consumer, the performance optimization pattern, and the honest answer to 'Context vs Redux' that interviewers want to hear.
React Context is a broadcast channel inside your component tree. Any component that subscribes to a context receives its current value directly — without props being manually threaded down through every intermediate layer. The Provider is the transmitter; useContext is the receiver. Change the broadcasted value and every subscriber re-renders automatically. Context is designed for 'ambient' data — the current user, theme, locale, or feature flags that many components need but shouldn't have to receive as props.
Prop drilling happens when a piece of data needs to reach a deeply nested component, so you pass it as a prop through every layer in between — even layers that don't use it at all:
// App passes 'user' down through Layout → Sidebar → UserMenu
// Layout and Sidebar don't need user — they just relay it
function App() {
const user = useAuth();
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <span>{user.name}</span>; // finally used here
}
This is fragile — every intermediate component must be aware of data it doesn't care about. Context eliminates the middlemen.
// 1. Create the context — the argument is the default value
// (used only when a component has no Provider above it)
const UserContext = React.createContext(null);
// 2. Provide the value — any descendant can now read it
function App() {
const user = useAuth();
return (
<UserContext.Provider value={user}>
<Layout /> {/* no prop drilling — Layout doesn't receive user */}
</UserContext.Provider>
);
}
// 3. Any descendant reads it directly
function UserMenu() {
const user = useContext(UserContext); // zero props passed down
return <span>{user.name}</span>;
}
The component subscribes to the context. Whenever the Provider's value changes, UserMenu re-renders automatically.
This is the most common source of performance bugs with Context. Every component that calls useContext(MyContext) re-renders whenever the context value changes — even if the specific data it uses didn't change.
// ❌ Performance trap: a new object is created on every App render
// Every consumer re-renders even if user and theme haven't changed
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<AppContext.Provider value={{ user, theme, setUser, setTheme }}>
<Everything />
</AppContext.Provider>
);
}
The value prop creates a new object every render, so every consumer thinks the context changed — even if user and theme are identical. React uses Object.is to compare context values.
// ✅ Stable object reference — consumers only re-render when user or theme changes
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ user, theme, setUser, setTheme }), [user, theme]);
return (
<AppContext.Provider value={value}>
<Everything />
</AppContext.Provider>
);
}
// Split frequently-changing from rarely-changing values
// Components that only need theme don't re-render when user changes
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Everything />
</ThemeContext.Provider>
</UserContext.Provider>
Context is often misused as a replacement for a state management library. They serve different purposes:
cart.total doesn't re-render when user.preferences changes. They also come with devtools, time-travel debugging, and middleware.Context is not a state management library — it's a dependency injection mechanism. The question is not "Context or Redux?" but "how often does this value change and how many components subscribe to it?"
The default value passed to createContext(defaultValue) is used only when a component calls useContext with no matching Provider above it in the tree. It is not the initial value of the Provider. This trips up many developers:
const ThemeContext = React.createContext('light'); // default = 'light'
// This component uses 'light' because there's no Provider above it
function Orphan() {
const theme = useContext(ThemeContext); // 'light'
return <div>{theme}</div>;
}
// But this gets 'dark' from the Provider — the createContext default is ignored
function App() {
return (
<ThemeContext.Provider value="dark">
<Orphan /> {/* gets 'dark', not 'light' */}
</ThemeContext.Provider>
);
}Many developers treat Context as a drop-in replacement for Redux — Context is a dependency injection mechanism, not a state management library. It has no selector system, so every subscriber re-renders on any value change. For frequently-changing global state, an external store like Zustand or Redux is the right tool.
Many developers pass a new object literal directly to the value prop — this creates a new object reference on every render, triggering re-renders in all consumers even when the underlying data hasn't changed. Always memoize the value object with useMemo.
Many developers think the createContext default value is like an initial state — the default value is only used when useContext is called without a matching Provider anywhere above it in the tree. It's a fallback for orphaned consumers, not the starting value of the Provider.
Many developers wrap the entire app in a single all-purpose context with everything in it — this maximises re-renders because every change (user, theme, language, permissions) causes every consumer to re-render. Split contexts by update frequency so consumers only re-render for the values they actually care about.
Many developers think Consumer components are required for reading context — useContext() makes Consumer render-prop components completely unnecessary in functional components. useContext is simpler, more readable, and should always be preferred in functional components.
Many developers think Context solves all prop drilling problems — Context is the right tool when many unrelated components need the same data. When two siblings need to share state, lifting state to their common parent is simpler and more explicit than adding a Context.
Authentication: the current user and auth state are read by dozens of components (nav, profile, permissions checks). A UserContext at the app root eliminates passing user as a prop through every layout and page component.
Theming: a ThemeContext holding 'light' or 'dark' mode is consumed by every styled component and UI library wrapper. The theme rarely changes, making Context ideal — consumers re-render only on explicit theme toggle.
Internationalisation (i18n): the current locale and translation function live in a context so any component can call t('key') without receiving the translator as a prop.
Feature flags: a FeatureFlagContext holds the current user's enabled features, consumed by any component that needs to show or hide UI — without every intermediate component knowing about feature flags.
Compound component patterns: libraries like Radix UI and Headless UI use Context internally to share state between parent and child components (e.g. Tabs.Root shares selected tab state with Tabs.Trigger and Tabs.Content) without exposing it as props.
React Router: the router uses Context internally to share the current location, params, and navigation function — that's why useLocation() and useNavigate() work from any depth in the tree without prop threading.
No questions tagged to this topic yet.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.