正在加载,请稍候…

React State Management in 2026: Context vs Zustand vs Redux Toolkit

Compare React state management options: React Context, Zustand, and Redux Toolkit. Learn when to use each, performance considerations, and code examples for real-world scenarios.

React State Management in 2026: Context vs Zustand vs Redux Toolkit

"Should I use Context, Zustand, or Redux?" is one of the most-asked React questions. In 2026, the ecosystem has matured and the answer is clearer. Here's a comprehensive comparison.

The State Management Landscape

Solution Bundle Size Complexity Devtools Best For
React Context 0KB (built-in) Low Basic Theme, auth, locale
Zustand 1.1KB Low Chrome ext Most apps
Redux Toolkit ~11KB Medium Excellent Large apps, complex flows
Jotai 3.1KB Low Yes Atomic state
TanStack Query 13KB Medium Excellent Server state

React Context — When to Use It

Context is built-in and great for infrequently changing global data.

// ✅ Good use of Context: Theme
const ThemeContext = createContext<'light' | 'dark'>('light');

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ✅ Good use of Context: Current user (rarely changes)
const AuthContext = createContext<{ user: User | null; logout: () => void } | null>(null);

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}

Context Performance Problem

// ❌ Problem: ANY context value change re-renders ALL consumers
const AppContext = createContext({
  user: null,
  cart: [],         // Changes frequently
  theme: 'light',   // Rarely changes
  notifications: [], // Changes frequently
});

// When cart updates, <ThemeToggle /> re-renders even though it doesn't use cart!
function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext); // Subscribes to ALL changes
  return <button onClick={() => setTheme('dark')}>{theme}</button>;
}

// ✅ Solution: Split contexts by update frequency
const ThemeContext = createContext(null);    // Rarely changes
const UserContext = createContext(null);     // Rarely changes
const CartContext = createContext(null);     // Changes often
const NotificationContext = createContext(null); // Changes often

Use Context for: Theme, auth user, locale, feature flags — data that's global and doesn't change often.

Zustand — The Sweet Spot

Zustand is simple, small, and solves the Context re-render problem.

npm install zustand
// stores/cartStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  total: () => number;
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
      
      addItem: (newItem) => set((state) => {
        const existing = state.items.find(i => i.id === newItem.id);
        if (existing) {
          return {
            items: state.items.map(i => 
              i.id === newItem.id ? { ...i, quantity: i.quantity + 1 } : i
            )
          };
        }
        return { items: [...state.items, { ...newItem, quantity: 1 }] };
      }),
      
      removeItem: (id) => set(state => ({
        items: state.items.filter(i => i.id !== id)
      })),
      
      updateQuantity: (id, quantity) => set(state => ({
        items: quantity === 0 
          ? state.items.filter(i => i.id !== id)
          : state.items.map(i => i.id === id ? { ...i, quantity } : i)
      })),
      
      clearCart: () => set({ items: [] }),
      
      total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    }),
    { name: 'cart-storage' } // Persists to localStorage
  )
);
// Using Zustand — selectors prevent unnecessary re-renders
function CartIcon() {
  // Only re-renders when items.length changes
  const itemCount = useCartStore(state => state.items.length);
  return <span>{itemCount} items</span>;
}

function CartTotal() {
  // Only re-renders when total changes
  const total = useCartStore(state => state.total());
  return <span>${total.toFixed(2)}</span>;
}

function AddToCartButton({ product }) {
  const addItem = useCartStore(state => state.addItem);
  return <button onClick={() => addItem(product)}>Add to Cart</button>;
}

Zustand with TypeScript and DevTools

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useStore = create<Store>()(
  devtools(       // Chrome DevTools support
    persist(      // LocalStorage persistence
      (set) => ({
        // ... store definition
      }),
      { name: 'my-store' }
    ),
    { name: 'MyStore' } // DevTools display name
  )
);

Redux Toolkit — For Complex Apps

Redux Toolkit (RTK) modernizes Redux with much less boilerplate.

npm install @reduxjs/toolkit react-redux
// store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface UserState {
  currentUser: User | null;
  users: User[];
  loading: boolean;
  error: string | null;
}

// Async thunk with automatic pending/fulfilled/rejected actions
export const fetchUsers = createAsyncThunk(
  'users/fetchAll',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/users');
      if (!response.ok) throw new Error('Failed to fetch');
      return await response.json();
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const userSlice = createSlice({
  name: 'users',
  initialState: {
    currentUser: null,
    users: [],
    loading: false,
    error: null,
  } as UserState,
  
  reducers: {
    setCurrentUser: (state, action: PayloadAction<User>) => {
      state.currentUser = action.payload; // Immer allows "mutations"
    },
    logout: (state) => {
      state.currentUser = null;
    },
    updateUser: (state, action: PayloadAction<Partial<User> & { id: string }>) => {
      const index = state.users.findIndex(u => u.id === action.payload.id);
      if (index !== -1) {
        state.users[index] = { ...state.users[index], ...action.payload };
      }
    },
  },
  
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  },
});

export const { setCurrentUser, logout, updateUser } = userSlice.actions;
export default userSlice.reducer;
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import cartReducer from './slices/cartSlice';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

export const store = configureStore({
  reducer: {
    users: userReducer,
    cart: cartReducer,
  },
  // RTK includes redux-thunk and Immer automatically
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// Typed hooks (use these instead of raw hooks)
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

RTK Query — Data Fetching Built In

// store/api/usersApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const usersApi = createApi({
  reducerPath: 'usersApi',
  baseQuery: fetchBaseQuery({ 
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) headers.set('authorization', `Bearer ${token}`);
      return headers;
    },
  }),
  tagTypes: ['User'],
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => '/users',
      providesTags: ['User'],
    }),
    getUserById: builder.query<User, string>({
      query: (id) => `/users/${id}`,
      providesTags: (result, error, id) => [{ type: 'User', id }],
    }),
    updateUser: builder.mutation<User, Partial<User> & { id: string }>({
      query: ({ id, ...patch }) => ({ url: `/users/${id}`, method: 'PATCH', body: patch }),
      invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
    }),
  }),
});

export const { useGetUsersQuery, useGetUserByIdQuery, useUpdateUserMutation } = usersApi;

// Usage
function UserList() {
  const { data: users, isLoading, error } = useGetUsersQuery();
  // Automatic caching, background refetch, loading states!
}

Decision Guide

Is it local component state?
  → useState / useReducer

Is it server/async data (API responses)?
  → TanStack Query or RTK Query (NOT Zustand/Redux)

Is it shared global state?
  → Small/medium app: Zustand
  → Large app with complex flows: Redux Toolkit
  
Is it UI state that rarely changes? (theme, auth, locale)
  → React Context

Summary

In 2026:

  • Context: Theme, auth, locale. Simple, built-in, free. Avoid for frequently updated state.
  • Zustand: Sweet spot for most apps. Tiny, simple, fast, great DX.
  • Redux Toolkit: Large apps, complex async flows, need time-travel debugging.
  • TanStack Query/RTK Query: Server state. Use these instead of putting API responses in Redux.

→ Inspect and format your state snapshots with JSON Viewer.