State management: Zustand & Redux Toolkit
When local state and context are not enough: global stores with Zustand and Redux Toolkit.
You can build surprisingly large apps with useState, useReducer and context. But as apps grow, you may notice prop drilling, context providers that re-render too much, and update logic spread everywhere. State management libraries give you a central store that any component can read from and update, with fine-grained re-renders. This lesson covers the two you're most likely to meet: Zustand (small and simple) and Redux Toolkit (structured and battle-tested).
Do you need a library?#
Sort your state into kinds first:
Once server data lives in a data-fetching library and filters live in the URL, many apps have very little global state left. Reach for a store when shared client state is genuinely complex or updated from many places.
Zustand: a store in a few lines#
A store is a hook created with create. It holds state and the functions that update it:
setmerges the object you return into the state (one level deep), so you don't need to spread the rest.getreads the current state inside actions.
Use it in any component — no provider needed:
Selectors and re-renders#
The function you pass — (s) => s.items — is a selector. A component re-renders only when the value it selects changes (compared with Object.is). That's the big advantage over a single context value.
If you select several values into a new object, wrap the selector with useShallow, otherwise the new object counts as a change every time:
Persisting to localStorage#
Zustand's persist middleware saves the store and restores it on page load:
You can also read and update a Zustand store outside React — handy in utility code or tests:
Redux Toolkit: structure for big teams#
Redux keeps all global state in one store, updated only by dispatching actions to reducers — the same pattern as useReducer, at app scale. Modern Redux means Redux Toolkit (RTK), which removes the old boilerplate.
1. Create a slice
A slice bundles a piece of state, its reducers and auto-generated action creators:
itemAdded(product) creates { type: "cart/itemAdded", payload: product } for you.
2. Configure the store
configureStore also sets up the Redux DevTools extension and development checks that warn about accidental mutations.
3. Provide it and use hooks
useSelector works like Zustand selectors: the component re-renders only when the selected value changes.
Async logic with createAsyncThunk
For most API data, though, RTK ships RTK Query — a caching data-fetching layer similar to TanStack Query (next lesson).
TypeScript tip#
With TypeScript, derive types from the store and create pre-typed hooks once:
Zustand or Redux Toolkit?#
Other options you'll hear about: Jotai (atom-based, like many tiny useStates shared globally) and MobX (observable objects). All are valid — consistency within a project matters more than the choice.
Common mistakes#
- Putting everything in global state, including form inputs and server data.
- Selecting the whole store (
useCartStore(), oruseSelector((s) => s)), so components re-render on every change. - Returning a new object from a selector without
useShallow(Zustand) or a memoised selector (Redux'screateSelector). - Mutating state in a Zustand
setcall — Zustand doesn't use Immer by default, so return new objects. - Using
createAsyncThunkfor every request when a caching library would remove most of the code.
What's next#
Speaking of server state: next you'll learn TanStack Query, the standard tool for fetching, caching and updating API data.
Check your understanding
Quick quiz
1.In Zustand, why write
useCartStore((s) => s.items)instead ofuseCartStore()?2.In a Redux Toolkit
createSlicereducer, why can you writestate.items.push(item)?3.Which state usually should NOT go into a global client store?
Finished reading?
Mark this lesson complete to track your progress.