React State Management in 2024: Zustand, Jotai, TanStack Query & Context
Rating
Verdict
Use TanStack Query for all server state, Zustand for complex client state, and React Context for simple low-frequency globals (theme, locale). Avoid Redux unless you have very specific requirements.
Pros
- TanStack Query eliminates boilerplate for server state
- Zustand is simpler than Redux with no boilerplate
- Jotai atom model scales from simple to complex
- Context API is perfect for low-frequency global state
Cons
- No single solution fits all use cases
- Over-engineering state is a common trap
- Learning multiple libraries adds cognitive overhead
React State Management in 2024: Zustand, Jotai, TanStack Query & Context
State management remains one of the most debated topics in React development. The ecosystem has matured dramatically — the monolithic Redux approach has given way to specialized, purpose-built libraries. The key insight of modern React state management is the server/client state distinction: these are fundamentally different problems that require different solutions.
Categorizing Your State
Before choosing a library, classify your state:
- Server State — Data that lives on the server and is fetched asynchronously (posts, users, products). Has loading/error/stale states. Use TanStack Query.
- Global Client State — UI state shared across many components (shopping cart, notifications, auth user). Use Zustand or Jotai.
- Local Component State — State that belongs to one component (form values, toggle states). Use
useState/useReducer. - URL State — State that should be shareable/bookmarkable (filters, pagination, search query). Use URL search params.
TanStack Query (React Query)
TanStack Query is the gold standard for server state management. It handles caching, deduplication, background refetching, pagination, and optimistic updates:
// 1. Setup QueryClient
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // Data fresh for 1 minute
gcTime: 10 * 60 * 1000, // Cache for 10 minutes
retry: 2,
refetchOnWindowFocus: false,
},
},
});
export default function App() {
return (
// 2. Define typed query functions
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface Post { id: string; title: string; content: string; }
const postKeys = {
all: ['posts'] as const,
list: (filters?: Record
) =>
fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).then(r => r.json()),
onMutate: async (newPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: postKeys.all });
// Optimistically add the post
const optimisticPost = { ...newPost, id: 'temp-' + Date.now() };
queryClient.setQueryData(postKeys.list(), (old: any) => ({
...old,
posts: [optimisticPost, ...(old?.posts ?? [])],
}));
return { optimisticPost };
},
onError: (err, _, context) => {
// Roll back on error
queryClient.setQueryData(postKeys.list(), (old: any) => ({
...old,
posts: old?.posts?.filter((p: Post) => p.id !== context?.optimisticPost.id),
}));
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: postKeys.all });
},
});
}
// 4. Mutations with optimistic updates
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Omit
Zustand for Client State
Zustand is a minimal state management library with a simple API — no providers, no reducers, just stores:
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit
Jotai for Atomic State
Jotai uses an atomic state model — small pieces of state that compose together:
{filtered.length} resultsimport { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// Primitive atoms
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
const searchQueryAtom = atom('');
const selectedTagsAtom = atom
React Context: When to Use It
Context is ideal for low-frequency global values that rarely change (theme, locale, auth user). Avoid it for high-frequency state — every context change re-renders all consumers:
// Good context usage: auth user (changes once on login/logout)
interface AuthContextType {
user: User | null;
login: (credentials: LoginCredentials) => Promise
Conclusion
Modern React state management isn't about picking one library — it's about using the right tool for each category of state. TanStack Query for server state eliminates an enormous amount of boilerplate. Zustand for client state is simpler than anything that came before it. Jotai's atoms shine for highly granular, reactive state. And Context remains the right choice for simple global values. Combine them intelligently and your React applications will be both performant and maintainable.