Lesson 10 of 12
useReducer & State Patterns
You will learn how to manage related pieces of state with useReducer, which keeps every update in one . This stops separate useState calls from getting out of sync.
import { useReducer } from 'react'
// The reducer is a pure function: (state, action) => new state.
// No fetching, no timers, no mutation — just the next state.
const initialState = { items: [], filter: 'all', draft: '' }
function todosReducer(state, action) {
switch (action.type) {
case 'draft_changed':
return { ...state, draft: action.value }
case 'added':
if (!state.draft.trim()) return state
return {
...state,
draft: '',
items: [...state.items, { id: crypto.randomUUID(), title: state.draft, done: false }],
}
case 'toggled':
return {
...state,
items: state.items.map(t => (t.id === action.id ? { ...t, done: !t.done } : t)),
}
case 'deleted':
return { ...state, items: state.items.filter(t => t.id !== action.id) }
case 'filter_changed':
return { ...state, filter: action.filter }
default:
throw new Error('Unknown action: ' + action.type)
}
}
function Todos() {
const [state, dispatch] = useReducer(todosReducer, initialState)
const visible = state.items.filter(t =>
state.filter === 'all' ? true : state.filter === 'done' ? t.done : !t.done
)
return (
<>
<form onSubmit={e => { e.preventDefault(); dispatch({ type: 'added' }) }}>
<input
value={state.draft}
onChange={e => dispatch({ type: 'draft_changed', value: e.target.value })}
/>
<button>Add</button>
</form>
<ul>
{visible.map(todo => (
<li key={todo.id}>
<input type="checkbox" checked={todo.done}
onChange={() => dispatch({ type: 'toggled', id: todo.id })} />
{todo.title}
<button onClick={() => dispatch({ type: 'deleted', id: todo.id })}>×</button>
</li>
))}
</ul>
</>
)
}import { useReducer, useEffect } from 'react'
// Model state as a state MACHINE so impossible combinations cannot exist.
// Fragile: isLoading && error && data can all be true at once.
// const [data, setData] = useState(null)
// const [isLoading, setIsLoading] = useState(false)
// const [error, setError] = useState(null)
// Solid: exactly one status at a time.
const initial = { status: 'idle', data: null, error: null }
function fetchReducer(state, action) {
switch (action.type) {
case 'fetch_started': return { status: 'loading', data: null, error: null }
case 'fetch_succeeded': return { status: 'success', data: action.data, error: null }
case 'fetch_failed': return { status: 'error', data: null, error: action.error }
case 'reset': return initial
default: return state
}
}
function useFetchReducer(url) {
const [state, dispatch] = useReducer(fetchReducer, initial)
useEffect(() => {
const controller = new AbortController()
dispatch({ type: 'fetch_started' })
fetch(url, { signal: controller.signal })
.then(r => (r.ok ? r.json() : Promise.reject(new Error(r.status))))
.then(data => dispatch({ type: 'fetch_succeeded', data }))
.catch(error => {
if (error.name !== 'AbortError') dispatch({ type: 'fetch_failed', error })
})
return () => controller.abort()
}, [url])
return state
}import { createContext, useContext, useReducer } from 'react'
// Reducer + Context = shared state without prop drilling and without a library.
const CartContext = createContext(null)
const CartDispatchContext = createContext(null)
function cartReducer(state, action) {
switch (action.type) {
case 'added': {
const existing = state.items.find(i => i.id === action.item.id)
return existing
? { ...state, items: state.items.map(i =>
i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i) }
: { ...state, items: [...state.items, { ...action.item, qty: 1 }] }
}
case 'removed':
return { ...state, items: state.items.filter(i => i.id !== action.id) }
case 'cleared':
return { items: [] }
default:
return state
}
}
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, { items: [] })
// Two contexts: components that only dispatch do not re-render when the cart changes.
return (
<CartContext.Provider value={cart}>
<CartDispatchContext.Provider value={dispatch}>
{children}
</CartDispatchContext.Provider>
</CartContext.Provider>
)
}
export const useCart = () => useContext(CartContext)
export const useCartDispatch = () => useContext(CartDispatchContext)
function AddToCartButton({ product }) {
const dispatch = useCartDispatch()
return <button onClick={() => dispatch({ type: 'added', item: product })}>Add</button>
}Finished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.