State management in React has evolved dramatically. Redux dominated for years, but simpler alternatives like Zustand and Jotai have changed the conversation. Which should you choose in 2026?
| Feature | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| Bundle Size | ~11KB | ~3KB | ~3KB |
| Boilerplate | Medium | Low | Very Low |
| DevTools | Excellent | Good | Good |
| Learning Curve | Steep | Gentle | Gentle |
| Architecture | Flux/Single Store | Single Store | Atomic |
Zustand (German for "state") has become the most popular Redux alternative. It's tiny, unopinionated, and requires almost no boilerplate.
Zero Boilerplate: Create a store in 10 lines of code.
No Providers: Unlike Context or Redux, no need to wrap your app in providers.
TypeScript-First: Excellent type inference without manual typing.
Hook-Based: Feels native to React's hook patterns.
import { create } from 'zustand'; interface BearStore { bears: number; increasePopulation: () => void; removeAllBears: () => void; } const useBearStore = create<BearStore>((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })); // Usage in component function BearCounter() { const bears = useBearStore((state) => state.bears); return <h1>{bears} bears around here...</h1>; } function Controls() { const increasePopulation = useBearStore((state) => state.increasePopulation); return <button onClick={increasePopulation}>Add bear</button>; }
const useStore = create<Store>((set, get) => ({ users: [], loading: false, fetchUsers: async () => { set({ loading: true }); const users = await api.getUsers(); set({ users, loading: false }); }, }));
import { devtools, persist } from 'zustand/middleware'; const useStore = create<Store>()( devtools( persist( (set) => ({ // your store }), { name: 'bear-storage' } ) ) );
Redux Toolkit (RTK) modernized Redux, eliminating much of the boilerplate that made Redux infamous. It remains the choice for large, complex applications.
Structured Architecture: Clear patterns for organizing large applications.
RTK Query: Built-in data fetching and caching—no separate library needed.
DevTools: Best-in-class debugging with time travel.
Enterprise Adoption: More teams know Redux, easier hiring.
import { createSlice, configureStore } from '@reduxjs/toolkit'; const bearSlice = createSlice({ name: 'bears', initialState: { value: 0 }, reducers: { increasePopulation: (state) => { state.value += 1; }, removeAllBears: (state) => { state.value = 0; }, }, }); export const { increasePopulation, removeAllBears } = bearSlice.actions; const store = configureStore({ reducer: { bears: bearSlice.reducer }, }); // Usage requires Provider setup and useSelector/useDispatch function BearCounter() { const bears = useSelector((state) => state.bears.value); const dispatch = useDispatch(); return ( <> <h1>{bears} bears</h1> <button onClick={() => dispatch(increasePopulation())}>Add</button> </> ); }
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ getUsers: builder.query({ query: () => 'users' }), addUser: builder.mutation({ query: (user) => ({ url: 'users', method: 'POST', body: user }) }), }), }); export const { useGetUsersQuery, useAddUserMutation } = api;
Jotai takes a fundamentally different approach—instead of one store, you create individual atoms of state that compose together.
Atoms Are Primitive: Each piece of state is an independent atom.
Derived State: Create atoms that depend on other atoms.
No Providers (Usually): Works without wrapping your app.
React-Centric: Feels like useState that works across components.
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; // Primitive atoms const bearsAtom = atom(0); const hungerAtom = atom(5); // Derived atom const hungryBearsAtom = atom((get) => get(bearsAtom) > 0 && get(hungerAtom) > 7 ); // Usage function BearCounter() { const [bears, setBears] = useAtom(bearsAtom); return ( <> <h1>{bears} bears</h1> <button onClick={() => setBears(b => b + 1)}>Add bear</button> </> ); } function HungerStatus() { const areBearsHungry = useAtomValue(hungryBearsAtom); return <div>{areBearsHungry ? 'Feed the bears!' : 'Bears are content'}</div>; }
const usersAtom = atom(async () => { const response = await fetch('/api/users'); return response.json(); }); function UserList() { const users = useAtomValue(usersAtom); return users.map(user => <div key={user.id}>{user.name}</div>); }
All three are performant for typical applications. Differences emerge at scale:
Zustand: Selector-based—components only re-render when their selected state changes.
Redux: Same selector-based approach with optimized equality checks.
Jotai: Atom-based—components subscribe to specific atoms, enabling very fine-grained updates.
// Redux slice const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; } }, }); // Equivalent Zustand const useCounterStore = create((set) => ({ value: 0, increment: () => set((state) => ({ value: state.value + 1 })), }));
// Zustand store const useStore = create((set) => ({ count: 0, name: '', increment: () => set((s) => ({ count: s.count + 1 })), })); // Equivalent Jotai const countAtom = atom(0); const nameAtom = atom(''); const incrementAtom = atom(null, (get, set) => { set(countAtom, get(countAtom) + 1); });
For most new projects in 2026, Zustand is the pragmatic default. It's simple enough for small projects and scales well to medium-sized applications.
Redux Toolkit remains the best choice for large teams and complex enterprise applications where structure and tooling matter more than simplicity.
Jotai shines when you need atomic, composable state—think complex forms, real-time editors, or applications with many independent state pieces.
Start with Zustand, move to Redux Toolkit when you need more structure, and consider Jotai for specific use cases where atomic state is a natural fit.