The Complete Guide to State Management in Modern React in 2026
React's state management landscape fragmented into specialized solutions. Here's the honest comparison.
The State Management Spectrum
Not all state is the same:
- Server state: async, needs caching, mutation patterns — use React Query/TanStack Query
- UI state: local to component, ephemeral — use useState/useReducer
- Global UI state: modals, themes, sidebars — use Zustand/Jotai
- Server cache: prefetched data, optimistic updates — use TanStack Query
- URL state: filters, pagination — use nuqs
TanStack Query — Server State Done Right
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
});
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return <Profile user={data} />;
}
// Mutations with optimistic updates
function UpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['user'] });
const previous = queryClient.getQueryData(['user', newData.id]);
queryClient.setQueryData(['user', newData.id], newData);
return { previous };
},
onError: (err, newData, context) => {
queryClient.setQueryData(['user', newData.id], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['user'] });
}
});
}
Zustand — Simple Global State
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useStore = create(
devtools(
persist(
(set, get) => ({
user: null,
cart: [],
setUser: (user) => set({ user }),
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
total: () => get().cart.reduce((sum, item) => sum + item.price, 0),
clearCart: () => set({ cart: [] })
}),
{
name: 'app-storage',
part: ['user', 'cart'] // Only persist these
}
)
)
);
// Use in components
function CartButton() {
const { cart, total } = useStore();
return <button>{cart.length} items (${total()})</button>;
}
URL State for Filters and Search
import { useQueryState } from 'nuqs';
function ProductList() {
const [category, setCategory] = useQueryState('category');
const [sort, setSort] = useQueryState('sort', { defaultValue: 'price-asc' });
const [page, setPage] = useQueryState('page', { defaultValue: '1' });
// URL is now: /products?category=electronics&sort=price-asc&page=2
// Shareable, bookmarkable, back-button aware
}
Jotai — Atomic State Management
import { atom, useAtom } from 'jotai';
// Primitive atoms
const priceAtom = atom(10);
const quantityAtom = atom(1);
// Derived atoms (computed)
const totalAtom = atom((get) => get(priceAtom) * get(quantityAtom));
// Write-only atoms
const shippingAtom = atom(
null,
(get, set, update) => {
set(priceAtom, get.priceAtom + update);
}
);
function Cart() {
const [price] = useAtom(priceAtom);
const [quantity] = useAtom(quantityAtom);
const [total] = useAtom(totalAtom);
return <div>Total: ${total} ({quantity} items at ${price})</div>;
}
When to Use What
| Use Case | Solution |
|---|---|
| API data, caching | TanStack Query |
| Theme, modals | Zustand or Context |
| Forms | React Hook Form |
| URL params | nuqs |
| Animation state | useState + refs |
| Complex global state | Zustand or Redux Toolkit |
Conclusion
Stop using Redux for everything. TanStack Query handles server state, Zustand handles global UI state, and useState handles local state. Only reach for Redux when you have genuinely complex client-side logic that Zustand can't handle.
Build React apps faster with an all-in-one platform — includes templates, hosting, and state management patterns built-in.
Top comments (0)