React Context: From Basics to Advanced Patterns
The React Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It's React's built-in solution for state management that shines for certain types of "global" data.
The "Prop Drilling" Nightmare
Imagine a top-level component that fetches user data. Now, a deeply nested navigation bar needs to display the user's name. To get the data there, you'd have to pass the `user` prop through every single intermediate component, even if they don't use it.
// App.js
function App() {
const [user, setUser] = useState(null);
// ... fetch user
return <Layout user={user} />;
}
// Layout.js
function Layout({ user }) {
// Layout doesn't care about 'user'
return <Header user={user} />;
}
// Header.js
function Header({ user }) {
// Header doesn't care about 'user'
return <UserGreeting user={user} />;
}
// UserGreeting.js
function UserGreeting({ user }) {
return <span>Hello, {user.name}</span>;
}This is **prop drilling**. It's brittle (what if a component in the middle changes?), tedious, and makes components harder to reuse.
The Context API Solution (The 3 Steps)
Context solves this by creating a global "channel" for your data.
- 1. Create the Context:
You call `createContext()` outside your components. The value you pass is the **default value**, used only if a component tries to consume the context without a Provider.
const UserContext = React.createContext(null); - 2. Provide the Value:
Wrap a parent component (like `App`) with the context's `Provider` component. It takes a `value` prop.
<UserContext.Provider value={user}> <Layout /> </UserContext.Provider> - 3. Consume the Value:
Any child component, no matter how deep, can now read the value using the `useContext` hook.
// UserGreeting.js const user = useContext(UserContext); return <span>Hello, {user.name}</span>;
Notice `Layout` and `Header` are now clean—they don't need to know `user` exists.
Performance Deep Dive: The Gotchas
Context is powerful, but it has a major performance trap. **When a Context Provider's `value` prop changes, all components that consume that context will re-render.**
This is fine, but the trap is when you pass a new object or array to the `value` prop on every render:
❌ Bad Practice
function App() {
const [user, setUser] = useState(...);
// This object is new on EVERY render
return (
<UserContext.Provider value={{ user, setUser }}>
<RestOfApp />
</UserContext.Provider>
);
}This causes all consumers of `UserContext` to re-render, even if `user` and `setUser` haven't changed!
✔️ Good Practice
function App() {
const [user, setUser] = useState(...);
// Memoize the value
const providerValue = useMemo(() => (
{ user, setUser }
), [user, setUser]);
return (
<UserContext.Provider value={providerValue}>
<RestOfApp />
</UserContext.Provider>
);
}`useMemo` ensures the `providerValue` object is only created when `user` or `setUser` actually changes.
An even better pattern is to **split contexts**. If one part of your state changes often (like state) and one doesn't (like the setter function), put them in separate contexts.
const UserStateContext = createContext();
const UserDispatchContext = createContext();
// Provider
<UserStateContext.Provider value={user}>
<UserDispatchContext.Provider value={setUser}>
<RestOfApp />
</UserDispatchContext.Provider>
</UserStateContext.Provider>
// Component that only needs to *read* state:
const user = useContext(UserStateContext);
// Component that only needs to *update* state:
const setUser = useContext(UserDispatchContext);Now, a component that only *updates* state won't re-render when the `user` state itself changes.
Key Takeaway: Use Context for **low-frequency, global state** like authentication, theme, or user settings. For high-frequency state (like form inputs) or complex data flows, stick to local state or consider a dedicated state-management library.