Shared state
In short: Shared state in React comes from a provider high in the tree with Context and useReducer, and components read it directly, without props through every level.
Problem
The badge says 0 again
The cart is lifted into App, as in the lesson about lifting state up. From there it goes down as props: to Header and from it to CartBadge, to Catalog, from it to ProductList and from it to every ProductCard. Someone rewrote Header, and the badge stopped.
Console
Add bread, milk and bread again. The cards count, and the header says "Cart (0)". App passes count to Header, but the new Header does not pass it to CartBadge. Header does not use the cart. It only had to pass it on, and that is exactly what it forgot.
The idea
A provider at the top, consumers below
Passing through components that do not use the data is called prop drilling. With two levels it is normal. With five, every component on the way depends on the cart, and one missed prop breaks the chain.
Both libraries have a way to give data to distant descendants without the components in the middle:
- The provider is the component that owns the data. It says: "everything under me can read this".
- The consumer is the component that uses them. It takes them directly, no matter how many levels are in between.
In React this is Context:
export const CartContext = createContext(null);
// App.jsx<CartContext value={{ items, count, add }}> <Header /> <main><Catalog /></main></CartContext>
// CartBadge.jsxconst { count } = useContext(CartContext);createContext(null) makes the "channel". null is the value when there is no provider above the component. <CartContext value={…}> is the provider. useContext(CartContext) reads the value of the closest provider up the tree. When the value changes, every component that reads it renders again.
The data still have one owner, and the changes still go through it: add is the provider's function. Only the way changed.
See it
See the two ways
At first the cart goes down level by level, and the chain breaks in the middle. Then App becomes a provider and both consumers read directly.
const CartContext = createContext(null);
function App() { const [items, setItems] = useState([]); const count = items.reduce((s, i) => s + i.qty, 0); function add(product) { /* … */ } return ( <CartContext value={{ items, count, add }}> <Header /> <Catalog /> </CartContext> );}
function Header() { return <header><CartBadge /></header>;}
function CartBadge() { const { count } = useContext(CartContext); return <a href="/cart">Cart ({count})</a>;}
function ProductCard({ product }) { const { add } = useContext(CartContext); return <button onClick={() => add(product)}>Add</button>;}Step by step, in words
- The cart lives in App. Two components far below need it: CartBadge in the header and ProductCard in the catalog.
- With props only, the cart goes down level by level. Header, Catalog and ProductList do not use it. They only pass it on.
- Someone rewrites Header and forgets count. The chain breaks in the middle, and the badge shows 0. That is prop drilling.
- Instead: App wraps the tree in <CartContext value={…}>. Everything under it can read the cart.
- CartBadge and ProductCard read it with useContext(CartContext). The components in between know nothing about the cart.
- A click on “Add” calls add(product). The function came from the provider, so the change happens in App.
- The cart in App changes, and every component that reads it draws again: the badge says 1, the card says 1.
- The data still have one owner, App. Only the way changed: not through every level, but straight from the provider.
The components in the middle know nothing about the cart now. The new Header can be rewritten as much as you like.
Try it
Remove the drilling
The cart goes down as props through Header and Catalog, and Header drops count. Give the cart from the top. Header, Catalog and ProductList get nothing about the cart, and CartBadge and ProductCard read it directly. The check adds bread, milk and bread and looks at the cards and the badge.
Console
Solution
App.jsximport { useState } from 'react';
import { CartContext } from './cart.js';
import Header from './Header.jsx';
import Catalog from './Catalog.jsx';
export default function App() {
const [items, setItems] = useState([]);
const count = items.reduce((sum, i) => sum + i.qty, 0);
function add(product) {
const found = items.find((i) => i.id === product.id);
if (found) setItems(items.map((i) => (i.id === product.id ? { ...i, qty: i.qty + 1 } : i)));
else setItems([...items, { ...product, qty: 1 }]);
}
return (
<CartContext value={{ items, count, add }}>
<Header />
<main>
<Catalog />
</main>
</CartContext>
);
}
Header.jsximport CartBadge from './CartBadge.jsx';
export default function Header() {
return (
<header>
<a href="/">The shop</a>
<CartBadge />
</header>
);
}
CartBadge.jsximport { useContext } from 'react';
import { CartContext } from './cart.js';
export default function CartBadge() {
const { count } = useContext(CartContext);
return <a href="/cart">Cart ({count})</a>;
}
Catalog.jsximport ProductList from './ProductList.jsx';
export default function Catalog() {
return (
<section>
<h1>Products</h1>
<ProductList />
</section>
);
}
ProductList.jsximport ProductCard from './ProductCard.jsx';
const PRODUCTS = [
{ id: 1, name: 'White bread', price: 1.2 },
{ id: 5, name: 'Fresh milk', price: 1.6 },
{ id: 7, name: 'White cheese', price: 7.9 },
];
export default function ProductList() {
return (
<ul>
{PRODUCTS.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</ul>
);
}
ProductCard.jsximport { useContext } from 'react';
import { CartContext } from './cart.js';
import { money } from './money.js';
export default function ProductCard({ product }) {
const { items, add } = useContext(CartContext);
const qty = items.find((i) => i.id === product.id)?.qty ?? 0;
return (
<li>
<span>{product.name}, {money(product.price)}</span>
{qty > 0 && <span>In the cart: {qty}</span>}
<button type="button" aria-label={`Add ${product.name}`} onClick={() => add(product)}>
Add
</button>
</li>
);
}
cart.jsimport { createContext } from 'react';
export const CartContext = createContext(null);
money.jsexport const money = (x) => '€' + x.toFixed(2);
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span:first-child { flex: 1; }
button { font: inherit; }
CartContext is already in cart.js. In App wrap Header and main in <CartContext value={{ items, count, add }}>. CartBadge reads const { count } = useContext(CartContext), ProductCard reads items and add and works out its own qty. Remove the cart props from Catalog and ProductList.
The check catches a provider that wraps only main: Header stays outside it, useContext returns null and CartBadge crashes. It also catches a value without count.
The check sees only the behaviour. If you pass count through Header once more, it will pass. The goal here is different: the middle should not know about the cart.
The idea
A store for the cart
The cart grows: add, remove, change the count, empty it. If every action is a function in App, App fills up with logic that is not its job. So the shared state moves into a separate file, a store: the data and everything that changes them, in one place.
React has the built-in hook useReducer for this. Instead of setItems(…) the components send an action (dispatch({ type: 'add', product })), and one function, the reducer, decides what happens to the cart:
function cartReducer(items, action) { switch (action.type) { case 'add': /* a new array with the product */ default: return items; }}
export function CartProvider({ children }) { const [items, dispatch] = useReducer(cartReducer, []); const add = (product) => dispatch({ type: 'add', product }); return <CartContext value={{ items, count, add }}>{children}</CartContext>;}CartProvider is the provider, and App only puts it around the tree. The consumers do not change: they still read useContext(CartContext). The reducer is a plain function: the old cart and an action go in, a new cart comes out. It touches nothing else, so it is easy to test.
Context and useReducer are built into React. There are also libraries for stores, for example Zustand and Redux Toolkit. They are not in the sandbox, and here we only mention them: the idea is the same, one source and actions that change it.
Console
cart.jsx: all the logic of the cart is there. CartBadge and ProductCard are the same as in the task.Try it
Find the bug
TypeError: Cannot destructure property 'count' of … as it is null. Which line is to blame?Pick the line with the bug.
<Header /> is outside <CartContext value={…}>. CartBadge inside it looks for a provider up the tree and does not find one, so useContext returns null from createContext(null). The provider must wrap Header too.The idea
When shared and when not
Shared state is handy, and that is why it is tempting to put everything there. But anything in it can be read and changed from anywhere, and that makes bugs harder to find. The rule from the lesson about lifting holds: as high as needed, and no higher.
- Shared: what distant parts of the tree need and what lives while the user is on the site. The cart, the logged-in user, the language, the theme.
- In the component: the fields of a form, whether the menu is open, which tab is chosen, the text in the search box. One component (or its parent) needs them, and they go away with it.
- The server's data are not shared state. The product list and the orders have an owner, and the owner is the server. In a store they are a copy that must be refreshed. The component that shows them loads them, as in the lesson about data from an API.
Shared state does not change the direction: the data start from one owner, and the changes go through its functions.
Try it
Fill in
CartBadge read the count from the provider?import { useContext } from 'react';
import { CartContext } from './cart.js';
export default function CartBadge() {
const { count } = (CartContext);
return <a href="/cart">Cart ({count})</a>;
}useContext: it reads the value of the closest <CartContext value={…}> above the component and renders it again when that value changes.More precisely, if you are curious
<CartContext.Provider>. Before React 19 the provider was written <CartContext.Provider value={…}>. That form still works, and <CartContext value={…}> is the new, shorter one.
Your own hook. Next to a context there is often a small function export function useCart() { return useContext(CartContext); }. It can also throw a clear error when there is no provider, instead of a TypeError about null.
Who renders again. Every component that reads the context renders again when its value changes. value={{ items, count, add }} is a new object on every render of the provider. In a big application this is guarded with useMemo or with several smaller contexts. In the lesson about speed you will see how to measure it.
Class components read a context with static contextType or with <CartContext.Consumer>. You meet them in old code.
The store lives while the page is open. On a reload it is lost. A cart that must survive is saved in localStorage or on the server.
The checks see behaviour. The tasks here cannot see whether the cart comes from props, from a context or from a store. They check that the badge and the cards show the same thing.
Exercise
The cart page
In the main part there is CartSummary: a heading The cart, rows White bread × 2 with a Remove button, Total: €4.00, and The cart is empty when it is empty. Now it keeps its own cart that nobody fills. Add removing and a total to the store and make CartSummary read the cart from there. The check adds three products, removes them and looks at the badge, the cards and the total.
Console
Solution
App.jsximport { CartProvider } from './cart.jsx';
import Header from './Header.jsx';
import Catalog from './Catalog.jsx';
import CartSummary from './CartSummary.jsx';
export default function App() {
return (
<CartProvider>
<Header />
<main>
<Catalog />
<CartSummary />
</main>
</CartProvider>
);
}
cart.jsximport { createContext, useReducer } from 'react';
export const CartContext = createContext(null);
function cartReducer(items, action) {
switch (action.type) {
case 'add': {
const found = items.find((i) => i.id === action.product.id);
if (found) return items.map((i) => (i.id === found.id ? { ...i, qty: i.qty + 1 } : i));
return [...items, { ...action.product, qty: 1 }];
}
case 'remove':
return items.filter((i) => i.id !== action.id);
default:
return items;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const add = (product) => dispatch({ type: 'add', product });
const remove = (id) => dispatch({ type: 'remove', id });
return <CartContext value={{ items, count, total, add, remove }}>{children}</CartContext>;
}
Header.jsximport CartBadge from './CartBadge.jsx';
export default function Header() {
return (
<header>
<a href="/">The shop</a>
<CartBadge />
</header>
);
}
CartBadge.jsximport { useContext } from 'react';
import { CartContext } from './cart.jsx';
export default function CartBadge() {
const { count } = useContext(CartContext);
return <a href="/cart">Cart ({count})</a>;
}
Catalog.jsximport ProductList from './ProductList.jsx';
export default function Catalog() {
return (
<section>
<h1>Products</h1>
<ProductList />
</section>
);
}
ProductList.jsximport ProductCard from './ProductCard.jsx';
const PRODUCTS = [
{ id: 1, name: 'White bread', price: 1.2 },
{ id: 5, name: 'Fresh milk', price: 1.6 },
{ id: 7, name: 'White cheese', price: 7.9 },
];
export default function ProductList() {
return (
<ul>
{PRODUCTS.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</ul>
);
}
ProductCard.jsximport { useContext } from 'react';
import { CartContext } from './cart.jsx';
import { money } from './money.js';
export default function ProductCard({ product }) {
const { items, add } = useContext(CartContext);
const qty = items.find((i) => i.id === product.id)?.qty ?? 0;
return (
<li>
<span>{product.name}, {money(product.price)}</span>
{qty > 0 && <span>In the cart: {qty}</span>}
<button type="button" aria-label={`Add ${product.name}`} onClick={() => add(product)}>
Add
</button>
</li>
);
}
CartSummary.jsximport { useContext } from 'react';
import { CartContext } from './cart.jsx';
import { money } from './money.js';
export default function CartSummary() {
const { items, total, remove } = useContext(CartContext);
return (
<section aria-labelledby="cart-title">
<h2 id="cart-title">The cart</h2>
{items.length === 0 && <p>The cart is empty</p>}
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name} × {item.qty}</span>
<button type="button" aria-label={`Remove ${item.name}`} onClick={() => remove(item.id)}>
Remove
</button>
</li>
))}
</ul>
<p>Total: {money(total)}</p>
</section>
);
}
money.jsexport const money = (x) => '€' + x.toFixed(2);
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span:first-child { flex: 1; }
button { font: inherit; }
In cart.jsx add an action case 'remove': return items.filter((i) => i.id !== action.id);, a function remove = (id) => dispatch({ type: 'remove', id }) and total, and put them in value. CartSummary reads const { items, total, remove } = useContext(CartContext) and calls remove(item.id).
The check catches a reducer that removes with splice and returns the same array: React sees no change and nothing is drawn. It also catches a CartSummary placed outside CartProvider.
The check also catches a copy of the cart in CartSummary: it is empty when made and does not follow the store.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Derived state · Lifting state up · Data from an API in a component · Forms in a component
- Continues in: Routing · From the sandbox to a project · The shop front
- Tasks on this topic: Effects, data and forms