Chapter 4 of 4
Beyond a Reducer
When local state is no longer enough, and what the libraries actually give you.
A reducer plus context covers a lot of ground. There is a point where a purpose-built library is the better tool, and it is worth knowing what that point looks like.
Client state versus server state
Most 'state management' problems are really two different problems wearing the same name.
- Client state - UI concerns your app owns: the open tab, the theme, a draft form.
useState,useReducerand context handle this well. - Server state - a cache of data that lives elsewhere: it goes stale, needs refetching, deduplication, retries and loading states. This is what TanStack Query and SWR are for.
The common client state libraries
- Zustand - a small store outside React, read with a selector so only components using that slice re-render. Minimal ceremony.
- Redux Toolkit - reducers, actions and immutable updates with a large ecosystem and excellent devtools. Worth it for big apps and big teams.
- Jotai / Recoil - atom-based, where state is split into small independently subscribed pieces.
// Zustand: a store and a selector
const useCartStore = create((set) => ({
items: [],
add: (item) => set((state) => ({ items: [...state.items, item] })),
}));
// Only re-renders when items.length changes
const count = useCartStore((state) => state.items.length);What the libraries add over context
- Selector-based subscriptions, so a component re-renders only for the slice it reads.
- State that lives outside the React tree, readable from non-React code.
- Devtools with a full action history and time travel.
- Middleware for persistence, logging and undo.
Write a counter reducer
Complete counterReducer so 'increment' adds one, 'decrement' subtracts one but never goes below zero, and 'reset' returns to 0. Unknown actions return the state unchanged.