29
Mar

React 学习笔记(四):状态管理进阶

avatar
ByKK
2025-03-29/5 Min Read
0
0
(AI总结) 当应用变得复杂时,组件内部的状态管理已经不够用了。本篇将介绍 React 中的状态管理方案,从 Context API 到专业的状态管理库,以及如何在实际项目中选择合适的状态管理策略。

随着应用规模的增长,组件间的状态共享变得越来越复杂。本篇将介绍 React 中的状态管理方案,以及如何在实际项目中选择合适的状态管理策略。

状态管理的选择策略

何时需要状态管理库?

  • 组件间状态共享:多个组件需要访问相同的数据
  • 复杂的状态逻辑:状态更新逻辑复杂,需要统一管理
  • 性能优化:避免不必要的重新渲染
  • 开发体验:更好的调试工具和开发体验

选择标准

  1. 项目规模:小项目用 Context,大项目用专业库
  2. 团队熟悉度:选择团队熟悉的方案
  3. 性能要求:对性能要求高的项目选择轻量级方案
  4. 调试需求:需要强大调试工具的项目选择 Redux

Context API 的局限性

虽然 Context API 可以解决状态共享问题,但它有一些局限性:

// Context API 的问题示例
import { createContext, useContext, useState } from 'react';
 
const AppContext = createContext(null);
 
function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
 
  // 问题:所有状态变化都会导致所有消费者重新渲染
  const value = { user, setUser, theme, setTheme, notifications, setNotifications };
 
  return (
    <AppContext.Provider value={value}>
      <Header />
      <Main />
      <Sidebar />
    </AppContext.Provider>
  );
}
 
function Header() {
  const { theme } = useContext(AppContext);
  // 当 user 或 notifications 变化时,Header 也会重新渲染
  return <header>Theme: {theme}</header>;
}

主要问题:

  • 性能问题:任何状态变化都会导致所有消费者重新渲染
  • 缺乏中间件:无法处理异步操作、日志记录等
  • 调试困难:缺乏专业的调试工具

Zustand:轻量级状态管理

Zustand 是一个轻量级的状态管理库,API 简单,学习成本低。

基本使用

import { create } from 'zustand';
 
// 定义 store
interface UserStore {
  user: User | null;
  login: (user: User) => void;
  logout: () => void;
}
 
const useUserStore = create<UserStore>((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}));
 
// 在组件中使用
function UserProfile() {
  const { user, logout } = useUserStore();
  
  if (!user) return <div>请先登录</div>;
  
  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <button onClick={logout}>退出登录</button>
    </div>
  );
}

复杂状态管理

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
 
interface TodoStore {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  removeTodo: (id: number) => void;
  setFilter: (filter: 'all' | 'active' | 'completed') => void;
  getFilteredTodos: () => Todo[];
}
 
const useTodoStore = create<TodoStore>()(
  devtools(
    (set, get) => ({
      todos: [],
      filter: 'all',
      
      addTodo: (text) => set((state) => ({
        todos: [...state.todos, { id: Date.now(), text, completed: false }]
      })),
      
      toggleTodo: (id) => set((state) => ({
        todos: state.todos.map(todo =>
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        )
      })),
      
      removeTodo: (id) => set((state) => ({
        todos: state.todos.filter(todo => todo.id !== id)
      })),
      
      setFilter: (filter) => set({ filter }),
      
      getFilteredTodos: () => {
        const { todos, filter } = get();
        switch (filter) {
          case 'active':
            return todos.filter(todo => !todo.completed);
          case 'completed':
            return todos.filter(todo => todo.completed);
          default:
            return todos;
        }
      },
    }),
    { name: 'todo-store' }
  )
);
 
// 使用示例
function TodoApp() {
  const { 
    todos, 
    filter, 
    addTodo, 
    toggleTodo, 
    removeTodo, 
    setFilter,
    getFilteredTodos 
  } = useTodoStore();
  
  const filteredTodos = getFilteredTodos();
 
  return (
    <div>
      <div>
        <input 
          type="text" 
          onKeyPress={(e) => {
            if (e.key === 'Enter') {
              addTodo(e.currentTarget.value);
              e.currentTarget.value = '';
            }
          }}
          placeholder="添加新任务..."
        />
      </div>
      
      <div>
        <button onClick={() => setFilter('all')}>全部</button>
        <button onClick={() => setFilter('active')}>进行中</button>
        <button onClick={() => setFilter('completed')}>已完成</button>
      </div>
      
      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Redux Toolkit:企业级状态管理

Redux Toolkit 是 Redux 官方推荐的工具集,简化了 Redux 的使用。

基本概念

import { createSlice, configureStore } from '@reduxjs/toolkit';
 
// 定义 slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
    loading: false,
    error: null,
  },
  reducers: {
    setUser: (state, action) => {
      state.user = action.payload;
    },
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
    setError: (state, action) => {
      state.error = action.payload;
    },
  },
});
 
// 异步 action
export const loginUser = (credentials) => async (dispatch) => {
  dispatch(setLoading(true));
  try {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials),
    });
    const user = await response.json();
    dispatch(setUser(user));
  } catch (error) {
    dispatch(setError(error.message));
  } finally {
    dispatch(setLoading(false));
  }
};
 
// 配置 store
const store = configureStore({
  reducer: {
    user: userSlice.reducer,
  },
});
 
// 在组件中使用
import { useSelector, useDispatch } from 'react-redux';
 
function LoginForm() {
  const dispatch = useDispatch();
  const { loading, error } = useSelector((state) => state.user);
 
  const handleSubmit = (e) => {
    e.preventDefault();
    const credentials = {
      email: e.target.email.value,
      password: e.target.password.value,
    };
    dispatch(loginUser(credentials));
  };
 
  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      <input name="email" type="email" placeholder="邮箱" />
      <input name="password" type="password" placeholder="密码" />
      <button type="submit" disabled={loading}>
        {loading ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

状态管理方案对比

特性Context APIZustandRedux Toolkit
学习成本中等
Bundle 大小0KB2KB15KB
性能一般
调试工具基础强大
中间件支持丰富
适用场景简单应用中小型应用大型应用

实际项目中的选择策略

小型项目(< 10 个页面)

  • 推荐:Context API + useState
  • 原因:简单直接,无需额外依赖

中型项目(10-50 个页面)

  • 推荐:Zustand
  • 原因:API 简单,性能好,调试工具完善

大型项目(> 50 个页面)

  • 推荐:Redux Toolkit
  • 原因:生态完善,团队协作好,调试工具强大

特殊场景

  • 实时数据:考虑 Zustand + WebSocket
  • 复杂表单:考虑 React Hook Form + Zustand
  • 服务端状态:考虑 TanStack Query + Zustand

最佳实践

1. 状态设计原则

// 好的状态设计
interface AppState {
  // 用户相关
  user: User | null;
  userPreferences: UserPreferences;
  
  // 应用状态
  theme: 'light' | 'dark';
  language: string;
  
  // 业务数据
  todos: Todo[];
  notifications: Notification[];
}
 
// 避免的状态设计
interface BadAppState {
  // 不要存储派生状态
  completedTodosCount: number; // 应该通过计算得出
  filteredTodos: Todo[]; // 应该通过选择器得出
}

2. 状态分离

// 按功能分离状态
const useUserStore = create()(/* 用户相关状态 */);
const useTodoStore = create()(/* 待办事项状态 */);
const useUISStore = create()(/* UI 状态 */);
 
// 避免单一大型 store
const useAppStore = create()(/* 所有状态混在一起 */);

3. 性能优化

// 使用选择器避免不必要的重新渲染
const useUserStore = create((set, get) => ({
  user: null,
  userProfile: null,
  userSettings: null,
}));
 
// 好的做法:只订阅需要的状态
function UserProfile() {
  const userProfile = useUserStore((state) => state.userProfile);
  // 只有 userProfile 变化时才重新渲染
}
 
// 避免的做法:订阅整个 store
function UserProfile() {
  const { userProfile } = useUserStore();
  // 任何状态变化都会重新渲染
}

总结

  • Context API:适合简单的状态共享,但要注意性能问题
  • Zustand:轻量级选择,API 简单,性能好
  • Redux Toolkit:企业级选择,功能强大,生态完善
  • 选择策略:根据项目规模和团队需求选择合适的方案
  • 最佳实践:合理设计状态结构,注意性能优化

掌握这些状态管理方案,你就能构建出可扩展、可维护的 React 应用。