React notes: context and reducer
This is not a full React tutorial. It focuses on two patterns that are commonly used together in real applications: Context and useReducer.
1) Context
Context is used to share data across multiple components without passing props through every level of the tree.
A common example is a shopping cart context.
import { createContext } from 'react';
type CartItem = {
id: number;
name: string;
quantity: number;
};
const CartContext = createContext<{ items: CartItem[] }>({
items: [],
});
The object passed to createContext() is the default value. This is only used when a component reads the context without a matching provider.
Provider
The provider is where we provide the actual value for the tree below it.
<CartContext.Provider value={{ items: cartItems }}>
<CartDetails />
</CartContext.Provider>
This value is not the default value. It is the live value React will expose to consuming components in the subtree.
Consuming the context
In a child component, we can read the value with useContext.
import { useContext } from 'react';
function CartDetails() {
const cartCtx = useContext(CartContext);
return <div>{cartCtx.items.length} items in cart</div>;
}
This is the standard pattern. The hook can be called at the top level of the component function.
In React 19, there is also a newer use(context) pattern available, but useContext is still the most common and easy to understand approach.
import { use } from 'react';
function CartDetails() {
const cartCtx = use(CartContext);
return <div>{cartCtx.items.length} items in cart</div>;
}
The important idea is the same: the component reads the current context value from the nearest provider.
Older pattern: Context.Consumer
Before hooks were common, React used a consumer pattern:
<CartContext.Consumer>
{(cartCtx) => {
return <div>{cartCtx.items.length} items in cart</div>;
}}
</CartContext.Consumer>
This works, but it is more verbose. useContext is the modern and simpler approach.
2) Context value should usually be connected to state
The value of the context is usually tied to some state in the parent component.
import { createContext, useContext, useState } from 'react';
const CartContext = createContext<{
items: { id: number; name: string }[];
addItem: (name: string) => void;
}>({
items: [],
addItem: () => {},
});
export default function App() {
const [items, setItems] = useState<{ id: number; name: string }[]>([]);
const addItem = (name: string) => {
setItems((prev) => [
...prev,
{ id: Date.now(), name },
]);
};
const ctxValue = {
items,
addItem,
};
return (
<CartContext.Provider value={ctxValue}>
<CartDetails />
</CartContext.Provider>
);
}
function CartDetails() {
const cartCtx = useContext(CartContext);
return (
<>
<div>Total items: {cartCtx.items.length}</div>
<button onClick={() => cartCtx.addItem('Apple')}>Add item</button>
</>
);
}
Now the shared value is linked to the component state. When state changes, all consumers of that context re-render.
This is one of the most common ways to manage shared application data in React.
3) Reducer
A reducer is a way to manage more complex state updates in a predictable way.
Instead of writing several setState calls with conditional logic, we define a reducer function that receives the current state and an action, and returns the next state.
useReducer also returns a state value and a dispatch function, similar to useState, but with one important difference: the update logic is centralized in a reducer function instead of being spread across multiple event handlers.
import { useReducer } from 'react';
type Action =
| { type: 'add'; item: string }
| { type: 'remove'; id: number }
| { type: 'clear' };
function cartReducer(state: string[], action: Action) {
switch (action.type) {
case 'add':
return [...state, action.item];
case 'remove':
return state.filter((_, index) => index !== action.id);
case 'clear':
return [];
default:
return state;
}
}
export default function App() {
const [items, dispatch] = useReducer(cartReducer, ['Apple']);
return (
<>
<button onClick={() => dispatch({ type: 'add', item: 'Orange' })}>
Add Orange
</button>
<button onClick={() => dispatch({ type: 'clear' })}>Clear</button>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</>
);
}
This is useful when the state can change in several ways and those changes are easier to reason about in one place. The reducer acts like a switchboard for state updates: the action tells it what happened, and the reducer decides the new state.
The reducer pattern is especially helpful when state updates are more than a simple assignment. It keeps the logic centralized and makes the flow easier to follow.
4) Context + reducer together
A very common pattern is to use a reducer inside a provider component and expose actions through context.
import { createContext, useContext, useReducer } from 'react';
type Item = { id: number; name: string };
type State = {
items: Item[];
};
type Action =
| { type: 'add'; item: string }
| { type: 'remove'; id: number };
function cartReducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return {
items: [
...state.items,
{ id: Date.now(), name: action.item },
],
};
case 'remove':
return {
items: state.items.filter((item) => item.id !== action.id),
};
default:
return state;
}
}
const CartContext = createContext<{
items: Item[];
addItem: (name: string) => void;
removeItem: (id: number) => void;
} | null>(null);
export default function App() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const value = {
items: state.items,
addItem: (name: string) => dispatch({ type: 'add', item: name }),
removeItem: (id: number) => dispatch({ type: 'remove', id }),
};
return (
<CartContext.Provider value={value}>
<CartView />
</CartContext.Provider>
);
}
function CartView() {
const cartCtx = useContext(CartContext);
if (!cartCtx) {
return null;
}
return (
<>
{cartCtx.items.map((item) => (
<button key={item.id} onClick={() => cartCtx.removeItem(item.id)}>
{item.name}
</button>
))}
<button onClick={() => cartCtx.addItem('Tea')}>Add Tea</button>
</>
);
}
This is a very common application structure:
- state is stored in a reducer
- actions describe what happened
- the provider exposes the current state and update functions
- child components consume the context without prop drilling
5) Summary
- Context shares data across components without manually passing props through many layers
- The provider supplies the actual value; the default value is only a fallback
-
useContextis the standard way to read context in functional components -
useReducerhelps manage more complex state updates in a predictable, centralized way - Context and reducer are often used together for app-level state like carts, auth, settings, and forms
A good rule of thumb is:
- use context for sharing data
- use reducer for managing state transitions
Together, they form a very solid pattern for building medium-size React apps.
Top comments (0)