
State management in React: Context, Zustand, or Redux?
React Context, Zustand or Redux Toolkit? When each state management option fits, and why server data usually belongs in TanStack Query instead.
In short: Start by separating server data from client state. Data from your API belongs in a library like TanStack Query, which handles caching and refetching. For the client state that remains, local state plus React Context for rarely changing values like theme or the logged-in user is often enough. Zustand is the lean choice for shared, frequently changing state, and Redux Toolkit pays off in large applications with complex logic and many developers.
Ask the right question first
In many React projects everything ends up in one global store: user data, product lists from the API, whether a modal is open, the contents of a form. That makes the store large, hard to test and slow. Most state management problems come from treating different kinds of state the same way.
It helps to distinguish four kinds:
- Server state: data that really lives on the server and is only cached, such as orders or customer lists.
- Global client state: data many components need but that exists only in the browser, such as a cart before checkout or UI preferences.
- Local state: anything that concerns one component, like whether a dropdown is open.
- URL state: filters, sorting and page numbers that belong in the address bar so links can be shared.
Server data: TanStack Query, not a store
For API data, a specialised library such as TanStack Query (formerly React Query) or SWR is almost always the better choice. It handles caching, loading states, error handling, background refetching and invalidation after mutations. Rebuilding that in Redux or Zustand means writing a lot of code for a solved problem.
In Next.js with server components, part of this moves to the server anyway: data is loaded during rendering and never needs to enter a client store. Our article on Next.js 15 covers this.
After that separation, surprisingly little global client state is usually left.
React Context: for infrequent changes
Context is built into React and needs no library. It works well for values many components read but that change rarely: theme, language, the logged-in user, feature flags.
The catch is that when a context value changes, every component using it re-renders. For frequently changing data, like input in a large form or real-time updates, that quickly causes noticeable performance problems. Splitting into several contexts and memoising helps, but beyond a certain point a dedicated library is simpler.
Zustand: lean and pragmatic
Zustand is a small library that gives you a global store with very little code. A store is a hook; components read only the parts they need through selectors and re-render only when those parts change.
const useCart = create<CartState>((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
const count = useCart((s) => s.items.length);
There is no provider, no classic actions and reducers, and little boilerplate. Middleware for persisting to localStorage or connecting Redux DevTools is available. For most small and medium applications, Zustand is where effort and benefit balance best.
Redux Toolkit: structure for large teams
Redux long had a reputation for boilerplate. Redux Toolkit, now the official way to write Redux, has cut that down considerably. Redux's strength is its structure: every change is a named action, state only changes through reducers, and the DevTools show every change with time-travel debugging.
That pays off in large applications with complex business logic and many developers, because everyone follows the same rules. Redux Toolkit also includes RTK Query for server data. For a small team and a modest application, that structure is often more than you need.
Whatever library you pick, store logic should be testable independently of components. Our post on testing strategies for React applications shows how, and the article on clean code in TypeScript covers solid typing.
Frequently asked questions (FAQ)
Do I need a state management library at all?
Often not. If server data goes through TanStack Query and filters live in the URL, local state and one or two contexts are enough for many applications. A library pays off once many components share the same frequently changing state.
Is Redux outdated?
No. Redux Toolkit is actively maintained and used in many large applications. For new, smaller projects, though, teams now more often pick lighter options like Zustand.
Can I combine Zustand and TanStack Query?
Yes, it is a very common pairing. TanStack Query manages everything that comes from the server, Zustand the pure client state. They do not get in each other's way.
Is it worth migrating from Redux to Zustand?
Only if Redux noticeably slows you down day to day. A gradual migration is possible because both can run side by side. Often it helps more to move server data out of the Redux store into TanStack Query or RTK Query first.
Conclusion
Picking the library is the smaller part of the decision. Separate server data, URL state and real client state cleanly and you need far less global state, and the right library usually follows from team size and complexity.
If you are planning a React application or want to tidy up an existing one, talk to us about web development.
Related posts
- React vs. Vue vs. Svelte: A Frontend Framework Comparison
React, Vue or Svelte for your next web project? A sober comparison of concepts, ecosystem, performance, hiring and long-term maintainability.
- Why we chose Next.js as our default web framework
Why we build web apps with Next.js by default: rendering options, SEO, TypeScript, self-hosting in Germany, and when we deliberately choose otherwise.
- Testing React Applications: Unit, Integration and E2E Tests
How to test React applications sensibly: which tests pay off, which tools fit, and how a test strategy catches bugs early instead of slowing the team down.
- Clean code in TypeScript: principles for maintainable projects
Practical clean code rules for TypeScript: strict types, clear names, small functions and validated boundaries, with examples and the mistakes to avoid.