01Start with the smallest container that works
State management has a reputation for being complicated, and the ecosystem has done nothing to simplify that impression. Redux, Zustand, Jotai, Recoil, MobX, Pinia, Nanostores — the list of options implies the problem is large. Usually it isn't. Most state bugs aren't architecture failures; they're data living somewhere too far from where it's needed, or somewhere too close when something else needs to reach it.
There's a straightforward mental model that cuts through most of this: every piece of state has a natural home, and your job is to find it before you reach for a library. The question isn't "which state manager should I use?" It's "who actually needs this data, and for how long?"
Local state is where almost everything should start. A dropdown that opens and closes, a form field mid-edit, a hover effect, a step in a multi-stage modal — none of this belongs in a global store. In React, that's useState or useReducer. In Vue, it's a ref or reactive inside the component. In Svelte, it's just a variable with let. Local state is fast, obvious, and dies when the component does. Its lifecycle is the component's lifecycle — which is almost always exactly right for UI-specific interactions.
A common mistake is promoting state upward the moment a second component touches it. Before you do that, ask whether those two components genuinely share a concern or just happen to need a similar value. A button label computed from a form field value isn't really shared state — it's derived state, and it belongs as close to the calculation as possible. Compute it locally; don't hoist it.
Local state is fast, obvious, and dies when the component does.
02Shared state: earn it first
When two components genuinely need the same data and neither is an ancestor of the other, you have shared state. This is where most devs reach for a global store immediately, but there's a middle step worth trying: component composition. If you own the parent, consider whether the shared data can live there and flow down. Context — React's createContext, Vue's provide/inject, Svelte stores scoped to a subtree — is often enough before you need a dedicated state library.
When you do need a real store, keep it small and domain-scoped. Don't create one monolithic object that holds everything the app knows. A store for authentication state, a separate one for the current user's preferences, another for a specific feature's workflow — each has a clear owner and a clear purpose. Libraries like Zustand and Pinia make this easy precisely because they encourage small, focused stores over the one-giant-object-to-rule-them-all pattern that made Redux painful to navigate.
The acid test for shared state: if you deleted this store, which components would break? If the answer is "almost all of them," your store is doing too much. If the answer is "two related components in one feature," you've found the right shape.
03Server state is a different beast
Here's the category that trips people up most: data that lives on a server, gets fetched, cached, and refreshed. This isn't really your state — it's a local copy of the server's truth, and it has a lifecycle you don't fully control. Loading flags, error states, cache invalidation, background re-fetching, pagination cursors — managing all that by hand with useEffect and a couple of state variables is where codebases quietly fall apart.
The right move is to treat server state as its own category and use tooling built for it. React Query (TanStack Query), SWR, and Vue Query are purpose-built for this. They handle caching, deduplication, stale-while-revalidate, and retries in a way that a hand-rolled solution almost never gets fully right. The surface area of your own code shrinks dramatically, and the behaviour becomes more predictable, not less.
The payoff is that your "real" application state — the stuff that represents what a user is doing right now — becomes much smaller and easier to reason about, because all the async-data plumbing is handled.
