Every few months a new React project adds a state management library in week one, before there is any state worth managing.
The reasoning is always the same. "We will need it eventually, so let us set it up now." Six months later the app has three kinds of state, all forced through the same global store, and a new developer needs a diagram to understand where a simple loading flag lives.
The truth is that most React apps need far less state machinery than they install. Modern React, combined with a good server-cache library, handles the large majority of real applications without a global store at all. Knowing when you genuinely need one is a frontend architecture decision, and getting it right saves you from complexity that never pays for itself.
Here is how we decide.
First, Stop Treating State as One Thing
The single most useful move is to stop saying "state" as if it were one category. It is at least three, and they want different tools.
There is server state: data that lives in your database and is fetched over the network. Users, orders, posts, anything with a source of truth on the backend. There is UI state: whether a modal is open, which tab is active, what a form field currently holds. And there is global client state: things many parts of the app need that do not come from the server, like the current theme or the authenticated user object.
Most of the pain in React apps comes from treating server state as if it were client state, storing fetched data in a global store, and then hand-writing all the caching, refetching, and invalidation logic that a purpose-built tool would give you for free. Separate the three and the right tool for each becomes obvious.
Server State Wants a Cache, Not a Store
If your data comes from an API, it does not belong in Redux or Zustand. It belongs in a server-state library like React Query or SWR.
The reason is that server data has properties client state does not. It goes stale. It needs refetching. It can be loading, succeeded, or errored. Two components might request the same data and you do not want two requests. You want caching, deduplication, background refresh, and retry logic. Writing that by hand on top of a global store is a large amount of code that is easy to get subtly wrong, and it is exactly what these libraries exist to handle.
In practice, moving server data out of a global store and into React Query removes a startling amount of code from most apps. The loading and error states stop being manually managed booleans scattered everywhere and become properties the library hands you. We go deeper on this in Our Next.js Data Fetching Strategy.
So the first question in the framework is simple. Is this data from the server? If yes, use a server-cache library and stop there. You may find you never needed a global store at all.
UI State Wants to Stay Local
The second category, UI state, wants to live as close to where it is used as possible.
Whether a dropdown is open, what a form input holds, which step of a wizard the user is on. This is what useState is for, and it should stay in the component that owns it. The instinct to lift everything up to a global store "so it is all in one place" is the instinct that produces unmaintainable apps. Locality is a feature. When state lives next to the component that uses it, you can understand that component in isolation, delete it cleanly, and reason about it without tracing a global object.
The moment to lift state up is when two or more sibling components genuinely need to share it, and even then you lift it only to their nearest common parent, not to the top of the app. This is standard React composition, and for a large share of apps it is all the UI state management you will ever need.
Context Is for Low-Frequency Global Values
Between local state and a full library sits React Context, and it is widely misused in both directions.
Context is the right tool for values that are genuinely global but change rarely. The current theme, the authenticated user, the app locale. You set them high in the tree and read them anywhere without threading props through ten layers.
Where Context goes wrong is when people use it for high-frequency state. Because any context value change re-renders every consumer, putting rapidly-changing state in Context, like a value that updates on every keystroke, causes performance problems that look mysterious until you find the cause. Context is a delivery mechanism for stable global values, not a state manager. Use it for the theme and the user, not for the fast-moving stuff.
So When Do You Actually Reach for a Library?
After server state has gone to a cache, UI state has stayed local, and stable globals live in Context, what is left?
Genuinely global client state that changes often and is read in many places. This is rarer than people assume, but it is real. A complex multi-step builder where many disconnected components manipulate a shared draft. A collaborative editor with intricate shared client state. A large app with cross-cutting client concerns that Context would re-render into the ground.
That is when a dedicated store like Zustand or Redux earns its place. And notice the shape of the decision: you reach for the library because you have identified a specific problem it solves, not because you might have one someday. The library is the answer to a question your app is actually asking.
When you do reach for one, prefer the simplest tool that fits. Zustand gives you a small global store with almost no ceremony and is the right default for most cases that need a store at all. Redux, with its larger surface area and stricter patterns, earns its weight in genuinely large applications with complex state logic and many contributors who benefit from its structure and tooling. Choosing Redux for a small app is how you end up with more boilerplate than features.
The Cost of Reaching Too Early
It is worth being blunt about why premature state libraries hurt, because the cost is not obvious on day one.
Every piece of state you route through a global store is state a new developer has to understand globally rather than locally. Components stop being self-contained. Testing gets harder because components now depend on store setup. The "one place for everything" that sounded tidy becomes a single tangled object that everything reaches into, and changing one slice risks re-rendering or breaking things far away. The complexity does not announce itself. It accumulates, and then one day the app is hard to work in and nobody can point to the decision that made it that way.
Under-engineering state is cheap to fix. You add a store the day you actually need one, and it takes an afternoon. Over-engineering it is expensive to unwind, because by then everything depends on the structure you regret. This is the same principle we apply across the stack, and it is a big part of why we plan architecture before writing the first component, which we cover in React Frontend Architecture.
The Migration Trap: Adding a Store to Fix the Wrong Problem
There is a specific moment worth watching for, because it is where teams add a global store to solve a problem the store will not actually fix.
An app starts feeling slow or hard to reason about, someone concludes that the state is a mess, and the proposed remedy is a state management library to "get it under control". Sometimes that is right. Often it is not, and the real cause is one of the mistakes above: server data being manually managed instead of cached, or high-frequency values sitting in Context and re-rendering the tree, or state lifted far higher than it needed to be. Adding a global store on top of those problems does not remove them. It relocates them into a new abstraction and adds ceremony around the same underlying mess.
Before adding a store to fix a struggling app, diagnose what is actually wrong. If the pain is around fetched data, move it to a cache library and watch how much of the perceived state complexity simply evaporates. If the pain is re-renders, find what is changing in Context and pull it out. If components are entangled, the fix is usually better composition, not a central store they all reach into. Only once those are ruled out is the remaining problem the one a store is designed for. Reaching for the library first is how apps end up with a global store and the original problems, now harder to see. When we take over a codebase that "needs Redux", more often than not it needs the three categories separated cleanly, and the store turns out to be optional.
The Framework in One Pass
When you are deciding where a piece of state should live, walk the questions in order. Is it server data? Use a cache library and stop. Is it used by one component or a small local cluster? Keep it local with useState. Is it a stable global value like theme or user? Context. Is it genuinely global, client-owned, and frequently changing across many components? Now, and only now, a store.
Most apps that follow this end up with React Query doing the heavy lifting, local state everywhere it belongs, a thin Context or two, and often no global store at all. That is not under-building. That is a React app you can still reason about a year from now, which is the entire point of good frontend architecture.