React state management is one of the most debated topics in the ecosystem, and also one of the most over-engineered. Most applications need far less state management than developers think. This guide cuts through the noise.
The Most Important Distinction: Server State vs Client State
Before choosing a library, you need to answer one question: is this server state or client state?
Server state is data that lives on a server. User profiles, projects, tasks, invoices -- anything fetched from an API or database. Server state has specific characteristics: it is asynchronous, it can be stale, it needs to be refreshed, and multiple components might need the same data simultaneously.
Client state is data that lives only in the browser. UI state like which tab is active, whether a modal is open, form values before submission, the current theme preference. Client state is synchronous and does not need to be fetched.
The reason this distinction matters: most state management libraries (Zustand, Redux, MobX) are designed for client state. Using them to manage server state creates redundant work: you fetch from the server, store in the library's store, then manage loading states, error states, and cache invalidation yourself. Libraries like TanStack Query exist specifically to handle server state, and they do it far better.
A common mistake is managing server state with a client state library. This is the source of much of the complexity that gives state management its reputation for being difficult.
TanStack Query: The Most Underused Pattern
TanStack Query (formerly React Query) manages server state: fetching, caching, background refetching, optimistic updates, and cache invalidation. If your application fetches data from an API, you need a server state library.
function Projects() {
const { data, isLoading, error } = useQuery({
queryKey: ["projects"],
queryFn: () => fetch("/api/projects").then(r => r.json()),
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage />;
return <ProjectList projects={data} />;
}
What you get for free: automatic caching (same data is not refetched if it was recently loaded), background refetching (data updates when the user returns to the tab), deduplication (if two components request the same data simultaneously, one request is made), and loading/error states without manual useState.
Mutations with cache invalidation:
const queryClient = useQueryClient();
const createProject = useMutation({
mutationFn: (data: NewProject) => fetch("/api/projects", { method: "POST", body: JSON.stringify(data) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["projects"] });
},
});
After a successful mutation, the projects list is refetched automatically. No manual state updates.
Zustand: The Right Choice for Client State
For global client state that is genuinely client-side (UI state, preferences, state shared across routes that does not belong in a server), Zustand is the best option in 2026. It is minimal, has no boilerplate, works with or without React context, and scales from small to large apps.
import { create } from "zustand";
interface UIStore {
sidebarOpen: boolean;
toggleSidebar: () => void;
activeTab: string;
setActiveTab: (tab: string) => void;
}
const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
activeTab: "overview",
setActiveTab: (tab) => set({ activeTab: tab }),
}));
No providers, no actions, no reducers. Use the store directly in any component.
Redux Toolkit: Still Valid for Complex Apps
Redux Toolkit (RTK) is not obsolete. For large applications with complex state interactions, RTK's devtools (time-travel debugging, action history) and predictable update model are still valuable. RTK also has RTK Query, which provides server state management similar to TanStack Query.
The case for RTK in 2026: your team is already experienced with Redux, you need the devtools for debugging complex state flows, or you have a large codebase with many interdependent state updates.
The case against: new projects, small-to-medium apps, teams new to state management, or any situation where server state is the primary concern. RTK adds significant conceptual overhead compared to Zustand.
Jotai: For Fine-Grained Reactivity
Jotai uses an atomic model: small pieces of state (atoms) that components subscribe to individually. Only components that use a specific atom rerender when that atom changes.
This is valuable when you have many small, independent pieces of state and you want to avoid unnecessary rerenders from coarser-grained stores. Jotai is a good fit for complex form state, real-time collaborative features, or any situation where render performance is constrained by state granularity.
For most apps, Zustand is simpler and sufficient. Reach for Jotai when you have a specific render performance problem that Zustand's model exacerbates.
Context API: Fine for Low-Frequency Updates
React Context is not a state management solution. It is a dependency injection mechanism. Using it for high-frequency updates (every keystroke, every frame) causes performance problems because every consumer rerenders on every context change.
Context works well for: authentication state (changes rarely), theme preferences (changes rarely), user settings (changes rarely), and any global value that does not change often.
Do not use Context for: form state, list filters, pagination state, or anything that changes frequently. For those, use Zustand or TanStack Query depending on whether it is client or server state.
The Decision Tree
Small app (single developer, under 20 components): useState + TanStack Query for data. No global client state library needed.
Medium app (team of 2-5, 20-100 components): Zustand for client state + TanStack Query for server state. This handles the vast majority of real applications.
Large app (team of 5+, 100+ components, complex state interactions): Redux Toolkit with RTK Query, or Zustand + TanStack Query with clear conventions for what goes where. The choice depends more on team familiarity than technical superiority.
What to Avoid in 2026
Bare Redux (without Redux Toolkit): The boilerplate (action creators, action types, switch-case reducers) is unnecessary. RTK provides the same guarantees with a fraction of the code.
MobX for new projects: MobX is mature and well-designed, but the ecosystem momentum is with Zustand and TanStack Query. New projects have fewer reasons to choose it.
Managing server state with client state libraries: Storing fetched data in Zustand or Redux and manually managing loading states, error states, and cache invalidation. Use TanStack Query for this.
Global state for everything: Not every piece of state needs to be global. Keep state as local as possible. Lift it when two or more components need the same state. Go global only when necessary.
Keep Reading
- TypeScript for React Developers: Practical Patterns That Actually Help -- typing your state stores correctly
- React Server Components: What They Are and When to Use Them -- how server state fits into RSC data fetching
- Next.js App Router Patterns in 2026: What to Use and What to Avoid -- where state management fits in the App Router model
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace -- chat, projects, time tracking, AI meeting summaries, and invoicing -- in one tool. Try it free.