Skip to content
状态管理

状态管理

当应用状态跨多个组件共享且层级复杂时,需要状态管理方案。本章由简到繁介绍三种模式:Context + useReducer、Zustand(轻量)、Redux Toolkit(重量)。

Vue 用户对照:Context ≈ provide/inject,Zustand ≈ Pinia。


方案选型速查

方案 学习曲线 包体积 适用规模 DevTools
Context + useReducer 0 小~中
Zustand ~2KB 小~大 ✅ (Redux DevTools)
Jotai ~3KB 小~大
Redux Toolkit ~11KB 中~大 ✅ (最完善)
MobX ~16KB 中~大

💡 最佳实践:新项目默认选择 Zustand。它 API 极简、TypeScript 友好、性能好。只有团队历史原因或极度复杂的表单场景才考虑 Redux Toolkit。


方案一:Context + useReducer

适合小到中型应用,无需引入第三方库。

// store.js
import { createContext, useContext, useReducer } from 'react';

const StoreContext = createContext(null);
const DispatchContext = createContext(null);

const initialState = { todos: [], filter: 'all' };

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.payload] };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.payload ? { ...t, done: !t.done } : t
        ),
      };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

export function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StoreContext.Provider>
  );
}

export function useStore() {
  return useContext(StoreContext);
}
export function useDispatch() {
  return useContext(DispatchContext);
}

🚨 陷阱:Context 有一个致命问题——任何一个 Context value 变化,所有消费该 Context 的组件都会重渲染。将 state 和 dispatch 分别放两个 Context(如上所示),只消费 dispatch 的组件不会因 state 变化而重渲染。

Context + useReducer 的痛点

一个 Context value 变化
  ↓
所有 useContext 的组件重渲染
  ↓
即使该组件只用到了 value 中未变化的部分

解决方案:

  • 拆分 Context:按功能域拆分多个 Context(ThemeContextUserContextCartContext
  • 选择器 + 订阅:手动实现或用第三方库(use-context-selector)

方案二:Zustand(推荐)

npm install zustand

基础用法

import { create } from 'zustand';

const useBearStore = create((set, get) => ({
  bears: 0,
  fishes: 0,
  increaseBears: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  // get() 读取当前状态,无需作为依赖
  report: () => console.log(`Bears: ${get().bears}, Fishes: ${get().fishes}`),
}));

// 在组件中使用
function BearCounter() {
  const bears = useBearStore(state => state.bears);  // 选择器
  return <h1>{bears} bears</h1>;
}

// 也可在组件外使用
useBearStore.getState().increaseBears();

中间件

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

const useStore = create(
  devtools(
    persist(
      (set) => ({
        theme: 'light',
        setTheme: (theme) => set({ theme }),
      }),
      { name: 'app-storage' }  // localStorage key
    ),
    { name: 'AppStore' }  // DevTools 中的 display name
  )
);

解决 Context 重渲染问题

Zustand 的选择器机制天然避免无效重渲染:

// ✅ 只有当 bears 变化时才会重渲染
const bears = useBearStore(s => s.bears);

// ✅ 返回原始值(bears 变时重渲染)
const bears = useBearStore(s => s.bears);

// ⚠️ 返回对象 → 每次都是新引用 → 总是重渲染
const { bears, fishes } = useBearStore();  // 不要这样用

💡 最佳实践:使用选择器精确订阅最小状态片段。对象形式的选择器用 shallow 比较器:useBearStore(s => ({b: s.bears, f: s.fishes}), shallow)

异步 Action

const useUserStore = create((set, get) => ({
  user: null,
  loading: false,
  fetchUser: async (id) => {
    set({ loading: true });
    try {
      const user = await fetch(`/api/users/${id}`).then(r => r.json());
      set({ user, loading: false });
    } catch (error) {
      set({ loading: false });
    }
  },
}));

方案三:Redux Toolkit(RTK)

npm install @reduxjs/toolkit react-redux

核心概念

概念 说明
Slice 状态切片 = reducer + actions 的集合
Store 单一状态树
Selector 从 store 中读取特定数据的函数
Dispatch 发送 action 触发状态变更

完整示例

// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    incremented: state => { state.value += 1; },
    decremented: state => { state.value -= 1; },
    added: (state, action) => { state.value += action.payload; },
  },
});

export const { incremented, decremented, added } = counterSlice.actions;
export default counterSlice.reducer;

// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

// App.jsx
import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

// Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { incremented } from './store/counterSlice';

function Counter() {
  const value = useSelector(state => state.counter.value);
  const dispatch = useDispatch();

  return <button onClick={() => dispatch(incremented())}>
    {value}
  </button>;
}

createAsyncThunk(处理异步)

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async () => {
    const response = await fetch('/api/users');
    return response.json();
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: { items: [], status: 'idle' },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, state => { state.status = 'loading'; })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchUsers.rejected, state => { state.status = 'failed'; });
  },
});

RTK Query(数据获取)

RTK 内置了 RTK Query,实现 API 缓存:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
  reducerPath: 'api',
  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;

// 在组件中
function UserList() {
  const { data, error, isLoading } = useGetUsersQuery();
}

React Query / TanStack Query(服务端状态)

服务端状态客户端状态是两类不同的东西:

类型 示例 用谁管理
客户端状态 当前选中项、UI 开关、表单输入 useState / Zustand / Redux
服务端状态 用户列表、订单数据、API 响应 React Query / SWR / RTK Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function UserList() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
}

function AddUser() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: (newUser) => fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(newUser),
    }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] }); // 刷新列表
    },
  });
}

💡 最佳实践:将服务端状态交给 React Query 管理,客户端状态交给 Zustand。大多数应用中,React Query 可以消除 70%+ 的全局状态代码。


选择决策树

状态只在当前组件内使用?
  → YES → useState

状态需要跨组件共享?
  → 父子间 → Props 下传
  → 兄弟/跨层级 →
      → 简单(1-3 个值) → Context
      → 复杂(多值、多更新) → Zustand
      → 团队已有 RTK 生态 → Redux Toolkit

数据来自服务端 API?
  → YES → React Query / SWR / RTK Query

需要持久化(localStorage)?
  → Zustand + persist 中间件