State Management
Context API, Zustand, Redux Toolkit — when to use each
React's built-in Context + useReducer covers most state management needs. For global state with better developer experience, Zustand is the leading lightweight option. Redux Toolkit is the standard for complex enterprise apps that need middleware, time-travel debugging, and RTK Query for data fetching.
Key Points
- Context API: share state without prop drilling — good for low-frequency updates (theme, auth, locale); expensive for high-frequency (every keystroke causes all consumers to re-render)
- Zustand: 1KB store without providers — create store → useStore hook → automatic subscription and re-render
- Redux Toolkit (RTK): official Redux — createSlice (actions + reducer), configureStore, RTK Query (data fetching + caching)
- RTK Query: data fetching solution built into RTK — defineApi, endpoints, auto-generated hooks, caching, invalidation
- Jotai: atom-based (like Recoil) — individual atoms instead of global store; great for derived state
- When to use Redux: large teams, complex state transitions, time-travel debugging, shared state across many unrelated components
- When to use Zustand: medium complexity, simpler API than Redux, most features without boilerplate
- When Context is enough: auth state, theme, feature flags — updated infrequently, used by many components
- Server state vs client state: data from APIs is server state — use TanStack Query or RTK Query, not useState
| Library | Bundle size | Boilerplate | DevTools | Best for |
|---|---|---|---|---|
| Context + useReducer | 0KB (built-in) | Low | None | Simple shared state |
| Zustand | ~1KB | Very low | Redux DevTools | Medium complexity |
| Redux Toolkit | ~15KB | Medium | Excellent | Complex / enterprise |
| Jotai | ~3KB | Minimal | Jotai DevTools | Atomic, derived state |
| TanStack Query | ~12KB | Low | Query DevTools | Server/async state |
State management: Zustand with devtools+persist, RTK Query with cache invalidation, auto-generated hooks
// Zustand — minimal store
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) => set(s => ({ items: [...s.items, item] })),
removeItem: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0),
}),
{ name: 'cart-storage' } // persist to localStorage
)
)
);
// Usage — no Provider needed
function Cart() {
const { items, removeItem, total } = useCartStore();
return <div>Total: {total()}</div>;
}
// Redux Toolkit — slice + RTK Query
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// RTK Query — auto-generates hooks, handles caching + invalidation
const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({ query: () => '/users', providesTags: ['User'] }),
getUser: builder.query<User, string>({ query: (id) => `/users/${id}`, providesTags: ['User'] }),
createUser: builder.mutation<User, Partial<User>>({
query: (body) => ({ url: '/users', method: 'POST', body }),
invalidatesTags: ['User'] // auto-refetch getUsers after create
})
})
});
export const { useGetUsersQuery, useCreateUserMutation } = userApi;
// Component — data fetching with RTK Query
function UserList() {
const { data: users, isLoading, error } = useGetUsersQuery();
const [createUser, { isLoading: creating }] = useCreateUserMutation();
if (isLoading) return <Spinner />;
if (error) return <Error />;
return users?.map(u => <UserRow key={u.id} user={u} />);
}Real-World Example
TanStack Query (formerly React Query) is the most popular server-state library — it provides caching, background refetching, stale-while-revalidate, and pagination with a simple API. RTK Query is equivalent but integrated into Redux. The key insight: most "state" in a React app is actually server state (users, orders, products) — dedicated server-state libraries handle it far better than useState+useEffect ever could.