Implementation 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(
(set, get) => ({
user: null,
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(
(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.
This article contains affiliate links. If you sign up through the links above, I may earn a commission at no additional cost to you.
Ready to Build Your AI Business?
Get started with Systeme.io for free — All-in-one platform for building your online business with AI tools.
Top comments (0)