State Management Without the Pain
Most state bugs trace back to one root: two sources of truth for the same thing. The fix is not a better library. It is a clear answer to "where does this live?" before you write the first line.
Article focus
5 state categories
One owner per piece of data
Key takeaways
- Server state, URL state, local state, and global state each have a natural home. Mixing them creates bugs.
- If you can derive it, derive it. If you must store it, store it once.
- Zustand replaces Context for most shared global state - less boilerplate, no provider nesting.
Why is state management still hard in 2026?
Because the question is not technical - it is architectural. Which layer owns this data? The answer determines everything that follows.
Every frontend framework has solved the "how" of state management. React has useState, useReducer, Context, and server components. Zustand, Jotai, and Valtio exist for global state. React Query and SWR handle server state beautifully. The problem is never a missing tool. It is not knowing which tool to reach for.
The result is the same pattern in every codebase I audit: a user object in Context, a duplicated subset in local state, a third copy derived from a prop, and a fourth fetched from an API. When the UI shows stale data, nobody knows which copy is the truth.
The fix is a decision tree. Before you write a single line of state code, answer: what kind of state is this? See the Frontend Architecture pillar for the full framework this tree fits into.
State Decision Flow
Click each state type to see its tool, description, and examples.
Server State
Tool: React Query / SWR
Data that lives on the backend. Fetch, cache, invalidate. Never duplicate into local state.
The five kinds of frontend state
Server state, URL state, local UI state, shared global state, and derived state. Each has a clear owner.
The rule is simple: every piece of data falls into exactly one of these buckets. If you catch yourself putting the same value in two buckets, you have a bug waiting to happen.
- Server state - data that lives on the backend. Fetch with React Query, SWR, or server components. Cache, refetch, invalidate. Never duplicate into local state.
- URL state - the current route, search params, hash. Owned by the router. Read with useSearchParams, useParams. Push updates via navigation.
- Local UI state - is this dropdown open? Which tab is selected? Owned by useState or useReducer near the component.
- Shared global state - theme, auth, preferences. Crosses many components. Owned by Zustand or Context near the root.
- Derived state - computed from other state. Use useMemo. If you can calculate it from existing state, do not store it separately.
The decision tree in practice
Start at the top: is it server data? If yes, use a data-fetching library. If no, is it URL-level? If yes, use the router. Continue down until you find the right home.
Here is the exact logic I run through for every new piece of state:
typescript
// Decision tree (run this mentally before every state addition)
// 1. Can I derive it from existing state?
// -> useMemo. Stop.
// 2. Does it come from the server?
// -> React Query / SWR / server component. Never cache locally.
// 3. Does it belong in the URL? (pagination, filters, selected item)
// -> Router search params or route params.
// 4. Is it needed by only one component or its immediate children?
// -> useState or useReducer in the closest common parent.
// 5. Is it needed by many unrelated components across the tree?
// -> Zustand store (or Context for simple cases).
// 6. Is none of the above? You probably don't need this state.When useState is the right answer (and when it is not)
useState is right when the state is local, transient, and only one component cares. It is wrong when two distant leaves need the same value.
useState is the default for a reason. It is simple, it is local, and it does not leak. A dropdown open state, a form input value, a toggle - these are perfect useState candidates.
The red flag is prop drilling beyond two or three levels. If ComponentA needs state from ComponentF, and they are separated by four layers of components that just pass props through, you have a structural problem. Either the state belongs in a shared context/store, or the component tree needs refactoring.
I use a simple heuristic: if I am passing a prop through more than two intermediate components that do not use it, I reach for either lifting the state up to a proper shared location or restructuring the tree.
Server state - the most common mistake
Do not put server data into local state. Fetch it with a dedicated library that handles caching, refetching, and invalidation.
The most common state management mistake I see is fetching data from an API and storing it in useState or Context. Now you are responsible for refetching, cache invalidation, loading states, error states, and stale data detection. That is a lot of code to write and test.
React Query (TanStack Query) and SWR handle all of this. They give you loading, error, and success states out of the box. They deduplicate requests. They refetch on focus. They let you invalidate queries when mutations happen. Combined with shared API contracts, this eliminates the most common class of frontend bugs.
tsx
// Instead of this:
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(id).then(setUser);
}, [id]);
// Do this:
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
// Mutation that auto-invalidates the cache:
const { mutate } = useMutation({
mutationFn: updateUser,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user'] }),
});Need help implementing this?
I build these systems for a living - let's work on yours.
Frontend Architecture & System Design
Structure so teams can ship. Clear boundaries, state strategy, and contracts. New features land cleanly; refactors stay low-risk.
Frontend Development
Production UIs in React, Next.js, or Vue- third-party and payment integrations included. Built for real traffic and maintained in production.
Your turn
- >Did this help you ship something?
- >Which part clicked the most for you?
- >Applying this at work? Share your experience.
Recommended blogs
Continue reading

Improving Next.js Lighthouse Without Killing the Design
How I chase Lighthouse and Core Web Vitals on a real Next.js portfolio without turning the UI into a gray wireframe.
Photo by Pixabay on Pexels
Read article
JavaScript Closures Explained: Why Your Functions Remember Everything
Learn JavaScript closures with interactive demos. Covers lexical scope, the var vs let loop bug, stale React hooks, memory leak patterns, and closure interview questions.
Reference photo by Asad Photo on Pexels
Read article
Discussion
Leave a comment
Thoughts, questions, corrections - all welcome.