📋 React Todo 应用实战:useReducer + useContext 状态管理全解析

134 阅读5分钟

一、核心概念先理清 🔍

在动手拆解项目前,我们得先吃透两个核心工具的底层逻辑 ——useReducer 和 useContext 是如何配合实现全局状态管理的。

1. useReducer:状态管理的 "规则制定者"

  • 核心作用:处理复杂状态逻辑,通过 "规则"(reducer)管理状态变化,比 useState 更适合多状态联动的场景。

  • 四大要素

    • initialState:初始状态(比如空的 todo 列表 []);
    • reducer:纯函数,接收当前状态和动作(action),返回新状态(核心!所有状态变化必须经它处理);
    • dispatch:触发状态变化的 "传令兵",通过传递 action(格式 {type: '操作类型', payload: '数据'})调用 reducer;
    • action:描述 "要做什么" 的指令,比如 {type: 'ADD_TODO', text: '买牛奶'}

2. useContext:组件通信的 "快捷通道"

  • 核心作用:解决跨层级组件通信问题,避免 "props drilling"(props 层层传递的麻烦)。

  • 三大步骤

    • createContext:创建一个 "上下文容器"(比如 TodoContext);
    • Context.Provider:在父组件中 "提供" 数据(通过 value 属性),所有子组件(无论层级)都能访问;
    • useContext:在子组件中 "获取" 上下文数据,直接使用父组件提供的状态和方法。

3. 黄金组合:useContext + useReducer

两者结合能实现全局应用级别的响应式状态管理

  • useReducer 负责 "管理状态变化的规则";
  • useContext 负责 "将状态和规则传递给所有需要的组件"。

二、项目实战:一步步搭建 Todo 应用 🛠️

步骤 1:创建上下文(TodoContext)

首先需要一个 "容器" 来存放全局状态,这一步通过 createContext 实现(通常单独放在 TodoContext.js 中):

import { createContext } from 'react';
// 创建上下文,默认值可设为 null 或初始结构
export const TodoContext = createContext(null);

👉 作用:定义一个全局可访问的数据容器,后续由 Provider 填充具体数据。

步骤 2:编写 reducer 规则(todoReducer.js)

reducer 是状态变化的 "指挥官",所有 todo 的增删改都要遵循它的规则:

// todoReducer.js
function todoReducer(state, action) {
  switch(action.type) {
    // 新增 todo:在原数组后追加新对象(保持状态不可变)
    case 'ADD_TODO':
      return [...state, {
        id: Date.now(), // 用时间戳做唯一标识
        text: action.text, // 从 action 中取输入文本
        done: false // 初始为未完成
      }];
    
    // 切换 todo 状态:找到对应 id,反转 done 值
    case 'TOGGLE_TODO':
      return state.map(todo => 
        todo.id === action.id ? {...todo, done: !todo.done} : todo
      );
    
    // 删除 todo:过滤掉对应 id 的项
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.id);
    
    // 默认返回原状态(防止未知 action 破坏状态)
    default:
      return state;
  }
}
export default todoReducer;

👉 细节:

  • 所有操作都不直接修改原状态(比如不用 push 而是用 [...state]),这是 React 状态管理的核心原则(保持状态不可变);
  • action.type 是字符串常量,避免拼写错误(实际项目中可抽成常量文件)。

步骤 3:封装自定义 Hook(useTodos.js)

为了简化状态使用,我们用自定义 Hook 封装 useReducer 和状态操作方法:

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

export function useTodos(initial = []) {
  // 用 useReducer 关联 reducer 和初始状态,得到当前状态 todos 和传令兵 dispatch
  const [todos, dispatch] = useReducer(todoReducer, initial);

  // 封装操作方法:对外暴露直观的 API(无需关心 dispatch 和 action 细节)
  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 };
}

👉 优势:

  • 隐藏 dispatch 的细节,组件只需调用 addTodo('xxx') 即可,不用手动写 action
  • 集中管理所有 todo 相关操作,后续修改逻辑只需改这里。

步骤 4:提供全局状态(App.jsx)

在根组件中用 TodoContext.Provider 把状态 "广播" 给所有子组件:

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

function App() {
  // 调用自定义 Hook 获取状态和方法
  const todosHook = useTodos();

  return (
    // 通过 Provider 把数据"喂"给上下文,子组件就能访问了
    <TodoContext.Provider value={todosHook}>
      <h1>Todo App 📝</h1>
      <AddTodo /> {/* 新增 todo 组件 */}
      <TodoList /> {/* 展示 todo 列表组件 */}
    </TodoContext.Provider>
  );
}

👉 关键:value 属性传递的是 todosHook(包含 todosaddTodo 等),这意味着所有子组件都能拿到这些数据。

步骤 5:封装上下文访问 Hook(useTodoContext.js)

为了避免在每个组件中重复写 useContext(TodoContext),单独封装一个 Hook:

// useTodoContext.js
import { useContext } from 'react';
import { TodoContext } from '../TodoContext';

// 直接返回上下文数据,简化组件中的使用
export function useTodoContext() {
  return useContext(TodoContext);
}

👉 作用:复用逻辑,后续若修改 Context 名称,只需改这一个文件。

步骤 6:实现功能组件(AddTodo 和 TodoList)

① AddTodo:新增 todo 组件

// AddTodo/index.jsx
import { useState } from 'react';
import { useTodoContext } from '../../hooks/useTodoContext';

const AddTodo = () => {
  // 本地状态:管理输入框文本
  const [text, setText] = useState('');
  // 获取全局方法:addTodo
  const { addTodo } = useTodoContext();

  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交
    if (text.trim()) { // 非空校验
      addTodo(text.trim()); // 调用全局方法新增 todo
      setText(''); // 清空输入框
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)} // 实时更新输入文本
        placeholder="请输入任务..."
      />
      <button type="submit">添加 ✚</button>
    </form>
  );
};

👉 亮点:本地状态(输入框文本)和全局状态(todo 列表)分离,职责清晰。

② TodoList:展示和操作 todo 列表

// TodoList/index.jsx
import { useTodoContext } from '../../hooks/useTodoContext';

const TodoList = () => {
  // 获取全局状态和方法:todos、toggleTodo、removeTodo
  const { todos, toggleTodo, removeTodo } = useTodoContext();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {/* 点击文本切换完成状态,用删除线表示已完成 */}
          <span
            onClick={() => toggleTodo(todo.id)}
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
          >
            {todo.text}
          </span>
          {/* 点击按钮删除 todo */}
          <button onClick={() => removeTodo(todo.id)}>删除 🗑️</button>
        </li>
      ))}
    </ul>
  );
};

👉 亮点:通过 todo.done 动态控制样式,直观反映任务状态;点击事件直接调用全局方法,无需关心状态更新细节。

三、总结:这套模式好在哪? 🚀

  1. 状态管理清晰:所有状态变化规则集中在 todoReducer 中,可预测、易调试;

  2. 组件通信高效:通过 useContext 实现跨层级数据共享,不用手动传递 props;

  3. 逻辑复用性强:自定义 Hook(useTodosuseTodoContext)封装重复逻辑,组件更简洁;

  4. 可扩展性高:若需新增功能(比如编辑 todo),只需在 reducer 中加一个 case 'EDIT_TODO',再在组件中调用即可。

通过这个 Todo 应用,我们能清晰看到 useReducer 和 useContext 如何分工协作:前者定规则,后者传数据,两者结合让全局状态管理变得简单可控。下次遇到复杂状态场景,不妨试试这套组合拳哦! 💪