React状态管理深度指南:useContext + useReducer + 自定义Hook构建Todo应用 🚀

186 阅读10分钟

React状态管理深度指南:useContext + useReducer + 自定义Hook构建Todo应用 🚀

跳跳熊猫头.gif

掌握React官方推荐的状态管理组合拳,告别混乱的状态传递!

React状态管理艺术

前言:React状态管理的进化之路

在React开发中,状态管理是构建复杂应用的核心挑战。从早期的类组件setState到函数组件的useState,再到如今的useReduceruseContext,React为我们提供了越来越强大的状态管理工具。

随着应用规模扩大,你会发现仅靠useState管理状态就像试图用勺子挖隧道🥄 - 它能工作,但效率低下!本文将带你深入探索React内置Hook的威力,特别是如何组合使用useContextuseReducer构建优雅的状态管理方案。

一、React状态管理核心武器详解 🧰

1. useState:基础状态管理

useState是React中最基础的状态管理Hook,实现了数据驱动状态,适用于简单的状态管理场景:

const [state, setState] = useState(initialState);
  • state:当前状态值
  • setState:更新状态的函数
  • initialState:初始状态值

使用示例:

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

适用场景:

  • 简单的组件内部状态
  • 不需要跨组件共享的状态
  • 状态更新逻辑简单的情况

2. useReducer:复杂状态管理利器

useReduceruseState的进阶版,特别适合管理包含多个子值或状态逻辑复杂的场景:

image.png

const [state, dispatch] = useReducer(reducer, initialArg, init);
  • reducer:纯函数,格式为(state, action) => newState
  • initialArg:初始状态或创建初始状态的参数
  • init:可选的初始化函数
  • dispatch:发送action的方法

reducer函数示例:

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

使用示例:

function Counter() {
  const [state, dispatch] = useReducer(reducer, {count: 0});
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({type: 'increment'})}>
        Increment
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>
        Decrement
      </button>
    </div>
  );
}

3. useContext:跨组件通信神器

useContext让你无需通过props逐层传递数据,就能在组件树的任何位置访问数据:

const value = useContext(MyContext);
  • MyContext:通过createContext创建的上下文对象
  • value:从最近的<MyContext.Provider>获取的当前值

完整使用流程:

// 1. 创建Context
const ThemeContext = createContext('light');

// 2. 提供Context值
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. 在深层组件中使用
function Toolbar() {
  return <ThemedButton />;
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#FFF' }}>按钮</button>;
}

4. useState vs useReducer:如何选择?

特性useStateuseReducer
适用场景简单状态复杂状态逻辑
状态结构单一值对象或多值
状态更新直接设置通过action描述变更
可维护性简单场景好复杂场景更优
可测试性一般高(纯函数reducer)
性能优化依赖React内部优化可手动优化dispatch
代码量多(需要定义reducer)

选择指南:

  • 使用useState当:
    • 状态是独立的基本类型值
    • 状态更新逻辑简单
    • 不需要跨组件共享状态
  • 使用useReducer当:
    • 状态是复杂对象
    • 下一个状态依赖前一个状态
    • 有复杂的状态更新逻辑
    • 需要共享状态给多个组件

二、项目架构设计:关注点分离原则 🏗️

一路长大.gif

在开始编码前,让我们规划一个清晰的项目结构。良好的组织是成功的一半!

image.png

src/
├── assets/              # 静态资源
├── components/          # 展示组件(纯UI)
│   ├── AddTodo.jsx      # 添加待办组件
│   └── TodoList.jsx     # 待办列表组件
├── hooks/               # 自定义Hook(状态逻辑)
│   ├── useTodoContext.js # Context访问Hook
│   └── useTodos.js      # 核心状态管理逻辑
├── reducers/            # reducer函数(状态更新规则)
│   └── todoReducer.js   # 待办事项reducer
├── context/             # Context定义
│   └── TodoContext.js   # 待办事项Context
├── App.jsx              # 应用入口
└── main.jsx             # 应用渲染入口

设计哲学:

  1. 关注点分离

    • UI组件只负责渲染
    • 自定义Hook管理状态逻辑
    • reducer定义状态更新规则
    • Context提供全局访问
  2. 单一职责原则

    • 每个文件/模块只负责一件事
    • 功能变更只需修改对应模块
  3. 可测试性

    • reducer是纯函数,易于单元测试
    • UI组件可独立于状态逻辑测试
  4. 可扩展性

    • 添加新功能只需扩展reducer
    • 状态逻辑复用简单

三、手把手实现Todo应用 ✨

屏幕录制_2025-07-16_204128.gif

1. 定义状态更新规则:reducer

文件路径src/reducers/todoReducer.js

// reducer是纯函数,给定当前状态和action,返回新状态
function todoReducer(state, action) {
  switch (action.type) {
    // 添加新待办事项
    case 'ADD_TODO':
      return [
        ...state, 
        {
          id: Date.now(), // 使用时间戳作为ID
          text: action.text, // 从action获取文本
          done: false // 初始未完成
        }
      ];
      
    // 切换待办事项完成状态
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id 
          ? {...todo, done: !todo.done} // 创建新对象,避免直接修改
          : todo
      );
      
    // 删除待办事项
    case 'REMOVE_TODO':
      // 过滤掉ID匹配的项
      return state.filter(todo => todo.id !== action.id);
      
    // 默认返回当前状态
    default:
      return state;
  }
}

export default todoReducer;

reducer设计要点:

  • 纯函数:相同输入永远得到相同输出
  • 不可变性:不直接修改state,返回新对象
  • 明确action类型:使用常量定义action.type
  • 单一职责:每个case处理一种状态变更

2. 创建自定义Hook封装状态逻辑

文件路径src/hooks/useTodos.js

import { useReducer } from 'react';
import todoReducer from '../reducers/todoReducer';

// 自定义Hook:封装待办事项状态逻辑
export function useTodos(initialState = []) {
  // 使用useReducer管理状态
  const [todos, dispatch] = useReducer(todoReducer, initialState);
  
  // 封装操作函数
  const addTodo = text => dispatch({ 
    type: 'ADD_TODO', 
    text 
  });
  
  const toggleTodo = id => dispatch({ 
    type: 'TOGGLE_TODO', 
    id 
  });
  
  const removeTodo = id => dispatch({ 
    type: 'REMOVE_TODO', 
    id 
  });
  
  // 返回状态和操作方法
  return {
    todos,        // 待办事项列表
    addTodo,      // 添加待办
    toggleTodo,   // 切换完成状态
    removeTodo    // 删除待办
  };
}

自定义Hook优势:

  • 逻辑复用:可在多个组件中使用
  • 关注点分离:UI与状态逻辑解耦
  • 可测试性:可独立测试状态逻辑
  • 封装复杂性:隐藏实现细节

3. 创建Context提供全局访问

文件路径src/context/TodoContext.js

import { createContext } from 'react';

// 创建Context对象
export const TodoContext = createContext();

// 提供默认值(可选,有助于开发工具显示)
TodoContext.defaultValue = {
  todos: [],
  addTodo: () => console.warn('addTodo function not implemented'),
  toggleTodo: () => console.warn('toggleTodo function not implemented'),
  removeTodo: () => console.warn('removeTodo function not implemented')
};

4. 创建快捷访问Context的Hook

文件路径src/hooks/useTodoContext.js

import { useContext } from "react";
import { TodoContext } from "../context/TodoContext";

// 创建快捷Hook,简化Context访问
export function useTodoContext() {
  const context = useContext(TodoContext);
  
  // 开发环境下检查Context是否存在
  if (process.env.NODE_ENV !== 'production' && !context) {
    throw new Error('useTodoContext must be used within a TodoProvider');
  }
  
  return context;
}

为什么需要这个Hook?

  • 简化访问:避免在每个组件中重复导入Context
  • 错误处理:提供有意义的错误提示
  • 类型安全:为TypeScript提供类型支持
  • 抽象实现:隐藏Context实现细节

5. 实现UI组件

添加待办组件:src/components/AddTodo.jsx
import { useState } from 'react';
import { useTodoContext } from '../hooks/useTodoContext';

const AddTodo = () => {
  // 本地状态管理输入框文本
  const [text, setText] = useState('');
  
  // 从Context获取addTodo方法
  const { addTodo } = useTodoContext();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 验证并添加待办
    if (text.trim()) {
      addTodo(text.trim());
      setText(''); // 清空输入框
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="add-todo-form">
      <input 
        type="text" 
        value={text} 
        onChange={e => setText(e.target.value)}
        placeholder="What needs to be done?"
        aria-label="Add new todo"
        className="todo-input"
      />
      <button 
        type="submit" 
        className="add-button"
        disabled={!text.trim()} // 禁用空提交
      >
        ➕ Add
      </button>
    </form>
  );
};

export default AddTodo;
待办列表组件:src/components/TodoList.jsx
import { useTodoContext } from '../hooks/useTodoContext';

const TodoList = () => {
  // 从Context获取状态和操作方法
  const { todos, toggleTodo, removeTodo } = useTodoContext();
  
  // 计算统计信息
  const totalCount = todos.length;
  const completedCount = todos.filter(t => t.done).length;
  
  return (
    <div className="todo-list-container">
      {todos.length === 0 ? (
        <p className="empty-message">🎉 No todos, add one to get started!</p>
      ) : (
        <>
          <ul className="todo-list">
            {todos.map(todo => (
              <li 
                key={todo.id} 
                className={`todo-item ${todo.done ? 'completed' : ''}`}
              >
                <span 
                  onClick={() => toggleTodo(todo.id)}
                  className="todo-text"
                  aria-label={todo.done ? 'Mark as incomplete' : 'Mark as complete'}
                >
                  {todo.text}
                </span>
                <button 
                  onClick={() => removeTodo(todo.id)}
                  className="remove-button"
                  aria-label="Remove todo"
                >
                  🗑️
                </button>
              </li>
            ))}
          </ul>
          
          <div className="todo-stats">
            <span>Total: {totalCount}</span>
            <span>Completed: {completedCount}</span>
            <span>Pending: {totalCount - completedCount}</span>
          </div>
        </>
      )}
    </div>
  );
};

export default TodoList;

6. 组装应用入口

文件路径src/App.jsx

import { TodoContext } from './context/TodoContext';
import { useTodos } from './hooks/useTodos';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
import './App.css';

function App() {
  // 使用自定义Hook初始化状态管理
  const todosHook = useTodos([]);
  
  return (
    // 提供Context值给所有子组件
    <TodoContext.Provider value={todosHook}>
      <div className="app-container">
        <header className="app-header">
          <h1>✨ Todo Master ✨</h1>
          <p>A React state management demo with useContext + useReducer</p>
        </header>
        
        <main className="app-main">
          <AddTodo />
          <TodoList />
        </main>
        
        <footer className="app-footer">
          <p>Built with React Hooks | {new Date().getFullYear()}</p>
        </footer>
      </div>
    </TodoContext.Provider>
  );
}

export default App;

四、数据流全景图与原理剖析 🌐

无字动图.gif

1. 完整数据流

graph LR
    A[App组件] -->|创建| B[useTodos Hook]
    B -->|使用| C[useReducer]
    C -->|依赖| D[todoReducer]
    A -->|提供| E[TodoContext.Provider]
    E -->|包裹| F[AddTodo组件]
    E -->|包裹| G[TodoList组件]
    F -->|使用| H[useTodoContext]
    G -->|使用| H[useTodoContext]
    H -->|获取| I[状态和方法]
    F -->|调用| J[addTodo]
    G -->|调用| K[toggleTodo/removeTodo]
    J -->|触发| L[dispatch]
    K -->|触发| L[dispatch]
    L -->|执行| D[todoReducer]
    D -->|返回| M[新状态]
    M -->|更新| C[useReducer]
    C -->|通知| E[重新渲染]
数据流详细解析
1. 初始化阶段(组件挂载时)
graph LR
    A[App组件] -->|创建| B[useTodos Hook]
    B -->|使用| C[useReducer]
    C -->|依赖| D[todoReducer]
    A -->|提供| E[TodoContext.Provider]
  1. App组件初始化

    • App.jsx中,调用useTodos自定义Hook初始化状态管理
    • 代码:const todosHook = useTodos([]);
  2. useTodos内部使用useReducer

    • useTodos内部调用useReducer(todoReducer, initial)
    • 创建初始状态和dispatch函数
    • 代码:
      const [todos, dispatch] = useReducer(todoReducer, initial);
      
  3. 提供Context

    • App组件通过<TodoContext.Provider>将状态和方法提供给子组件
    • 代码:
      <TodoContext.Provider value={todosHook}>
        {/* 子组件 */}
      </TodoContext.Provider>
      
2. 用户操作阶段(触发状态更新)
graph LR
    E[TodoContext.Provider] -->|包裹| F[AddTodo组件]
    E -->|包裹| G[TodoList组件]
    F -->|使用| H[useTodoContext]
    G -->|使用| H[useTodoContext]
    H -->|获取| I[状态和方法]
    F -->|调用| J[addTodo]
    G -->|调用| K[toggleTodo/removeTodo]
  1. 子组件访问Context

    • AddTodoTodoList组件通过useTodoContext访问Context值
    • 代码(在AddTodo.jsx中):
      const { addTodo } = useTodoContext();
      
  2. 用户触发操作

    • 在AddTodo组件中提交表单时调用addTodo
    • 在TodoList组件中点击待办时调用toggleTodoremoveTodo
    • 代码(AddTodo.jsx):
      const handleSubmit = (e) => {
        e.preventDefault();
        if (text.trim()) {
          addTodo(text.trim()); // 这里调用addTodo方法
          setText('');
        }
      };
      
3. 状态更新阶段(dispatch到reducer)
graph LR
    J -->|触发| L[dispatch]
    K -->|触发| L[dispatch]
    L -->|执行| D[todoReducer]
    D -->|返回| M[新状态]
    M -->|更新| C[useReducer]
  1. dispatch发送action

    • addTodo方法内部调用dispatch({type: 'ADD_TODO', text})
    • 代码(useTodos.js):
      const addTodo = text => dispatch({ type: 'ADD_TODO', text });
      
  2. reducer处理action

    • todoReducer接收当前状态和action,返回新状态
    • 代码(todoReducer.js):
      case 'ADD_TODO':
        return [
          ...state, 
          {
            id: Date.now(),
            text: action.text,
            done: false
          }
        ];
      
  3. 状态更新

    • useReducer接收到新状态,更新内部状态
    • 触发组件重新渲染
4. UI更新阶段(重新渲染)
graph LR
    C -->|通知| E[重新渲染]
  1. 状态变更通知
    • useReducer状态更新后,通知App组件重新渲染
  2. Provider传递新值
    • Context.Provider接收新值并传递给所有消费者
  3. 子组件更新
    • 所有使用该Context的子组件(AddTodo和TodoList)重新渲染
    • 显示更新后的状态
完整数据流示例:添加待办事项
  1. 用户在AddTodo组件输入"Buy milk"并提交
  2. AddTodo组件调用addTodo("Buy milk")
  3. addTodo方法调用dispatch({type: "ADD_TODO", text: "Buy milk"})
  4. dispatch触发todoReducer执行
  5. reducer处理ADD_TODO action,返回新状态数组
    // 原状态: []
    // 新状态: [{id: 123, text: "Buy milk", done: false}]
    
  6. useReducer更新内部状态,触发App组件重新渲染
  7. Context.Provider传递新状态给子组件
  8. TodoList组件接收新状态并重新渲染
  9. UI显示新添加的待办事项

2. 关键原理剖析

useReducer工作原理:

  1. 初始化时创建状态和dispatch函数
  2. 调用dispatch(action)时
  3. React将当前状态和action传递给reducer
  4. reducer返回新状态
  5. React使用新状态重新渲染组件

useContext工作原理:

  1. 创建Context对象时定义默认值
  2. Provider组件接收value属性
  3. 子组件使用useContext访问最近的Provider的value
  4. Provider的value变化时,所有使用该Context的子组件重新渲染

性能优化机制:

  • React使用Object.is比较新旧状态
  • 状态不变时不触发重新渲染
  • 合理使用useMemo/useCallback避免不必要的渲染

五、高级应用与最佳实践 🔍

1. 性能优化技巧

问题: Context值变化导致所有消费者重新渲染

解决方案:

// 拆分Context
const TodoStateContext = createContext();
const TodoDispatchContext = createContext();

// 提供者组件
function TodoProvider({children}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <TodoStateContext.Provider value={state}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

// 自定义Hook访问dispatch
function useTodoDispatch() {
  return useContext(TodoDispatchContext);
}

2. 持久化状态

// 使用localStorage持久化状态
function usePersistedTodos(key = 'todos') {
  const [state, dispatch] = useReducer(reducer, [], () => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : [];
  });
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [state, key]);
  
  return [state, dispatch];
}

3. 异步操作处理

// 在reducer中处理异步操作
async function fetchTodos(dispatch) {
  dispatch({type: 'FETCH_START'});
  
  try {
    const response = await fetch('/api/todos');
    const data = await response.json();
    dispatch({type: 'FETCH_SUCCESS', payload: data});
  } catch (error) {
    dispatch({type: 'FETCH_ERROR', error});
  }
}

// 在组件中使用
useEffect(() => {
  fetchTodos(dispatch);
}, []);

六、总结:为什么选择这种模式? 🏆

  1. 官方解决方案:无需第三方库,React内置支持
  2. 关注点分离:UI、状态逻辑和业务规则解耦
  3. 可维护性:代码组织清晰,易于理解和修改
  4. 可测试性
    • reducer是纯函数,易于单元测试
    • UI组件可单独测试
  5. 可扩展性
    • 添加新功能只需扩展reducer
    • 支持中间件模式
  6. 性能优化:精细控制重新渲染范围

Suggestion.gif

资源推荐:

希望本文帮助你深入理解React状态管理!如果有任何问题或想法,欢迎在评论区分享讨论💬。 !