State Management with Context API
In this page:
The Problem: Prop Drilling
Without shared state management, passing a piece of data from a top-level component down to a deeply nested one means passing it as a prop through every component in between, even ones that don't use it themselves. This is called 'prop drilling', and it makes refactoring painful.
Note: If you're passing the same prop through three or more component layers unchanged, that's a strong signal to reach for Context.
Warning: Not every piece of state needs Context — for state used by only a parent and its direct child, plain props are simpler and fine.
Example: The Problem: Prop Drilling
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function GrandChild({ user }) { return <p>Hello, {user}</p>; }
function Child({ user }) { return <GrandChild user={user} />; }
function App() {
const user = "Asha";
return <Child user={user} />;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Solving It with Context
Context lets a top-level Provider make a value available to any descendant, no matter how deeply nested, without passing it through every layer as a prop. Any component that needs the value calls useContext directly, skipping the components in between entirely.
Note: createContext() and the Provider are usually defined once, near the top of your app, and imported wherever needed.
Warning: A component using useContext must be rendered INSIDE the matching Provider, or it gets the context's default value instead.
Example: Solving It with Context
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const UserContext = React.createContext("Guest");
function GrandChild() {
const user = React.useContext(UserContext);
return <p>Hello, {user}</p>;
}
function Child() { return <GrandChild />; }
function App() {
return <UserContext.Provider value="Asha"><Child /></UserContext.Provider>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Combining Context with useState for Full State Management
Pairing Context with useState turns it into a lightweight but complete state-management solution: the Provider component holds the state and setter, and any descendant can both read the current value and update it, all without an external library.
Note: For state with more complex update logic, swap useState for useReducer inside the same Provider pattern — the Context part stays identical.
Warning: As an app grows large, a single Context holding many unrelated pieces of state can become as hard to manage as prop drilling was — split into focused contexts when this happens.
Example: Combining Context with useState for Full State Management
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const CountContext = React.createContext();
function CountProvider({ children }) {
const [count, setCount] = React.useState(0);
return <CountContext.Provider value={{ count, setCount }}>{children}</CountContext.Provider>;
}
function Counter() {
const { count, setCount } = React.useContext(CountContext);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
function App() {
return <CountProvider><Counter /></CountProvider>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Using one giant Context for all app state instead of splitting into focused contexts (auth, theme, cart).
- Forgetting that every consumer re-renders whenever the context value changes, even if they only use part of it.
- Not memoizing the Provider's value, causing unnecessary re-renders on every parent render.
- Context API state management uses createContext + useState + a Provider to share state app-wide.
- It avoids 'prop drilling' — manually passing state through many component layers that don't need it themselves.
- For simple to medium apps, Context + useReducer/useState is often enough without a separate library like Redux.
- Splitting state into multiple focused contexts limits how many components re-render on any given change.
Available since React 16.3 (stable Context API).
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: