State Management: Context API
So far, every piece of state you've seen has lived inside a single component with
useState (see State & Hooks), and any
data a component needed came in through props from its parent. That works fine
for small trees of components. But as an app grows — a logged-in user's identity, a selected
theme, an active language — you'll often have a piece of data that dozens of components,
scattered at every depth of the tree, all need to read. Passing that data down through props
alone quickly becomes painful. This lesson covers React's built-in Context API,
the standard solution for sharing data across a component tree without threading it through
every layer by hand.
The Problem: Prop Drilling
Prop drilling is the name for what happens when you pass a piece of data down
through several layers of components purely so it can reach a deeply nested child — even though
every component in between has no actual use for that data itself. Here's a realistic (if
slightly exaggerated) example: a theme value that needs to reach a
SubmitButton four components deep.
// BAD: theme has to be passed through every level, even though
// App, Screen, and Form never actually use it themselves
function App() {
const theme = 'dark';
return <Screen theme={theme} />;
}
function Screen({ theme }) {
// Screen doesn't use theme, just forwards it
return <Form theme={theme} />;
}
function Form({ theme }) {
// Form doesn't use theme either, just forwards it again
return <SubmitButton theme={theme} />;
}
function SubmitButton({ theme }) {
// finally, theme is actually used here
return (
<Button
title="Submit"
color={theme === 'dark' ? '#ffffff' : '#000000'}
/>
);
}
Every component between App and SubmitButton had to know about
theme just to relay it — Screen and Form are pure
middlemen. In a real app, add a second and third piece of shared data (current user, selected
language) and every one of those components needs even more pass-through props. Renaming a prop
means touching every level. This is exactly the problem Context solves.
Creating a Context
React.createContext() creates a "channel" that any component in the tree can tap
into, regardless of how deeply it's nested, without an unbroken chain of props connecting it to
the source:
import React from 'react';
const ThemeContext = React.createContext();
ThemeContext is just a plain JavaScript value at this point — creating it doesn't
share anything yet. Sharing happens in two steps: a Provider makes a value
available to the tree beneath it, and a consumer (via the useContext
hook) reads that value from anywhere inside.
Providing a Value at the Top of the Tree
Every context object comes with a .Provider component. Wrap it around the part of
your tree that should have access to the value, and pass the actual value through its
value prop:
function App() {
const theme = 'dark';
return (
<ThemeContext.Provider value={theme}>
<Screen />
</ThemeContext.Provider>
);
}
function Screen() {
// no theme prop needed at all anymore
return <Form />;
}
function Form() {
return <SubmitButton />;
}
Notice Screen and Form no longer mention theme at all —
the middleman problem is gone. Only the component that actually needs the value has to ask for
it.
Consuming a Context With useContext
Any descendant of the Provider — no matter how many levels down — can read the current value
with the useContext hook:
import React, { useContext } from 'react';
import { Button } from 'react-native';
function SubmitButton() {
const theme = useContext(ThemeContext);
return (
<Button
title="Submit"
color={theme === 'dark' ? '#ffffff' : '#000000'}
/>
);
}
This same pattern is exactly how you'd share a logged-in user's identity across an app — create
an AuthContext, provide the current user object once near the root, and read it
with useContext(AuthContext) from any screen that needs to show the user's name, or
decide whether to render a "Sign in" versus "Sign out" option — with zero prop threading through
every navigator and screen in between.
const AuthContext = React.createContext();
// near the root of the app
<AuthContext.Provider value={{ id: 'u_42', name: 'Sonu' }}>
<RootNavigator />
</AuthContext.Provider>
// anywhere deep inside RootNavigator
function ProfileScreen() {
const user = useContext(AuthContext);
return <Text>Welcome back, {user.name}</Text>;
}
Making Context Updatable: Combining useState With Context
Everything so far shared a value that never changed after the app started. Real apps usually
need shared state that updates over time — a shopping cart is a good example.
The trick is simple: put a useState call inside the component that renders the
Provider, and pass both the state value and a function to update it through
value.
import React, { createContext, useState } from 'react';
const CartContext = createContext();
function CartProvider({ children }) {
const [items, setItems] = useState([]);
function addItem(product) {
setItems(current => [...current, product]);
}
return (
<CartContext.Provider value={{ items, addItem }}>
{children}
</CartContext.Provider>
);
}
export default CartProvider;
export { CartContext };
CartProvider wraps the part of the app that needs cart access — typically near the
root, alongside or above your navigator:
function App() {
return (
<CartProvider>
<RootNavigator />
</CartProvider>
);
}
Now any screen, however deeply nested, can both read the cart and update it, without a single prop passed down:
import React, { useContext } from 'react';
import { View, Text, Button } from 'react-native';
import { CartContext } from './CartProvider';
function ProductScreen() {
const { items, addItem } = useContext(CartContext);
return (
<View>
<Text>Cart has {items.length} item(s)</Text>
<Button
title="Add running shoes"
onPress={() => addItem({ id: 'p_1', name: 'Running Shoes' })}
/>
</View>
);
}
Every call to addItem updates items inside CartProvider,
which re-renders the Provider with a fresh value — and every component consuming
CartContext automatically re-renders with the new data. This is the same
useState mechanics from State & Hooks;
Context just widens who's allowed to see and trigger the update.
When Context Is the Right Tool — and When It's Overkill
Context is convenient, but it isn't free. Every time the value passed to a Provider changes, every component consuming that context re-renders — even if it only cares about one small piece of a larger value object. For data that changes rarely, this is a non-issue. For data that changes constantly, it can quietly become a performance problem.
| Situation | Is Context a good fit? |
|---|---|
| Theme (light/dark) | Yes — changes rarely, read by many components |
| Logged-in user / auth status | Yes — changes rarely (login/logout), read broadly |
| Selected language / locale | Yes — changes rarely, read broadly |
| Text typed into a search box, updating on every keystroke | No — changes very frequently; every keystroke would re-render every consumer |
| A large, frequently-updated shopping cart or live data feed in a big app | Usually not ideal — works for small apps, but can cause broad re-renders as it scales |