Context + Reducer :通过实战实现两者的组合使用

82 阅读8分钟

在我之前的文章中,我已经分别对Context 和 Reducer 进行过详细的讲解,两种方法各有各的优势,那么,现在问你个问题,如果它们两种方法组合在一起,那又会发生什么呢?

今天,我将通过一个TodoList 实战,带你深入了解一下它们 “ 热血沸腾的组合技 ”。


一、Context 和 Reducer 的简单提要

1.1 Context : 跨层级通信的桥梁

Context 是一种无需手动传递 props ,就能让组件树中任意层级的组件直接访问祖先组件状态的方法。

它可以通过 createContext 创建上下文对象,再通过 Provider 将状态和方法传递给子组件,最后,子组件通过 useContext 就能使用上下文中的数据。

核心方法概括:

  • createContext(defaultValue) :创建一个上下文对象,其中包含ProviderConsumer
  • Provider:为组件树提供共享状态,可以通过value属性来传递数据。
  • useContext(context) :在函数组件中使用上下文数据。

使用场景

当多个层级的组件需要共享相同的状态(如主题、登录状态、Todos列表)时,Context能有效减少props的冗余传递,简化组件间的通信逻辑。


1.2 Reducer :复杂状态管理的利器

useReducer 是 React 提供的 Hook,用于管理复杂的状态逻辑。

它通过一个 纯函数(reducer) 来定义状态的更新规则,结合 dispatch 方法派发 action,实现状态的响应式更新。

核心方法概括:

  • useReducer(reducer, initialState) :React的Hook,用于返回当前状态和dispatch函数。
  • reducer(state, action) :纯函数,用于接收当前状态和action,返回一个新状态。
  • dispatch(action) :通过派发action事件,来触发状态更新。

使用场景

当状态逻辑较为复杂(如TodoList的添加、切换、删除操作),或需要依赖之前的状态值时,ReduceruseState更清晰且易于维护。


二、Context + Reducer 的组合实战

2.1 核心思路

上面我们已经分析了ContextReducer各自的优点,所以,在TodoList案例中,两者的分工已经显而易见了。

  • Context作为“管道”,负责跨层级传递状态和操作方法,将状态和操作方法注入到组件树中。
  • Reducer作为“控制台”,负责集中管理状态更新逻辑,统一处理所有状态变更请求。

这样用文字描述大家可能无法体会,下面,我将结合一个实战小案例,逐句来分析它们是如何协同工作的。

2.2 效果展示

为了更好地了解接下来的代码要做什么,我们先看看最终要实现的效果:

image.png

2.3 完整代码

为了不影响文章的观感,我将完整的代码放在文章最后面,有需要的读者朋友们可以自取。


三、超详细的代码分析

3.1 创建Context并注入状态

// App.jsx
import { TodoContext } from './TodoContext';
import { useTodos } from './hooks/useTodo';

function App() {
  const todosHook = useTodos(); // 初始化Todos状态
  return (
    <TodoContext.Provider value={todosHook}>
      <h1>Todo App</h1>
      <AddTodo />
      <TodoList />
    </TodoContext.Provider>
  );
}

效果说明:

  • 这段代码中,我们通过TodoContext.ProvidertodosHook(包含状态和操作方法)注入到整个组件树中。
  • useTodos() :这是一个自定义Hook,它封装了useReducer和业务逻辑,返回一个对象({ todos, addTodo, toggleTodo, removeTodo }),这个对象可以被Provider传递给所有子组件调用。

逐句分析

  1. const todosHook = useTodos();
    这句代码调用了useTodos Hook,初始化Todos的状态和操作方法,useTodos内部使用了useReducer,会返回当前Todos列表和操作函数。

  2. <TodoContext.Provider value={todosHook}>
    这句代码将todosHook作为值注入到Provider中,所有子组件都可以通过useContext来访问这个值,无需我们手动传递props。


3.2 Reducer处理状态更新逻辑

// reducers/todoReducer.js
function todoReducer(state, action) {
  switch (action.type) {
    case 'Add_Todo':
      return [
        ...state,
        {
          id: Date.now(),
          text: action.text,
          done: false,
        },
      ];
    case 'Toggle_Todo':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'Remove_Todo':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}
export default todoReducer;

效果说明

  • 这段代码实现了根据action.type来决定如何更新Todos列表。例如,添加任务时生成新ID,切换状态时修改done字段,删除任务时过滤掉指定ID。

  • 第一行的todoReducer是一个纯函数,它接收当前stateaction,返回一个新状态,达到不直接修改原始数据的目的。

逐句分析

  1. case 'Add_Todo'
    当接收到Add_Todo类型的action时,它会将新任务添加到state数组的末尾。

    • ...state:用于展开原数组,保留已有任务。
    • id: Date.now() :为新任务生成唯一ID(基于当前时间戳)。
    • text: action.text:从action.payload中提取任务文本。
  2. case 'Toggle_Todo'
    当接收到Toggle_Todo类型的action时,它会切换指定ID任务的完成状态。

    • state.map(...) :用来遍历所有任务,找到匹配ID的任务并更新其done字段。
    • { ...todo, done: !todo.done } :使用对象展开语法,创建新的任务对象,仅修改done字段。
  3. case 'Remove_Todo'
    当接收到Remove_Todo类型的action时,它会过滤掉指定ID的任务。

    • state.filter(...) :返回ID不匹配的任务数组,实现删除效果。

3.3 自定义Hook封装状态逻辑

// hooks/useTodo.js
export function useTodos(initial = []) {
  const [todos, dispatch] = useReducer(todoReducer, initial);
  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,
  };
}

效果说明

这段代码将useReducer和业务逻辑封装成一个可复用的Hook,提供给组件使用,通过dispatch派发action,触发Reducer处理状态更新。

逐句分析

  1. const [todos, dispatch] = useReducer(todoReducer, initial);

    • useReducer(todoReducer, initial) :初始化状态(initial默认为空数组),并绑定todoReducer作为状态更新规则。
    • todos:当前Todos列表的状态值。
    • dispatch:用于派发action的函数。
  2. addTodo(text)

    • dispatch({ type: 'Add_Todo', text }) :当用户输入任务文本并提交时,派发一个Add_Todo类型的action,携带任务文本。
    • text.trim() :确保任务文本不为空(在组件中已处理)。
  3. toggleTodo(id)

    • dispatch({ type: 'Toggle_Todo', id }) :点击任务时派发Toggle_Todo类型的action,携带任务ID,触发状态切换。
  4. removeTodo(id)

    • dispatch({ type: 'Remove_Todo', id }) :点击删除按钮时派发Remove_Todo类型的action,携带任务ID,触发删除逻辑。
  5. return { todos, addTodo, toggleTodo, removeTodo }

    • 返回一个对象,包含当前状态和操作方法,供Provider注入到组件树中。

3.4 子组件消费Context

// components/AddTodo.jsx
import { useTodoContext } from '../hooks/useTodoContext';

const AddTodo = () => {
  const [text, setText] = useState('');
  const { addTodo } = useTodoContext();
  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      addTodo(text.trim());
      setText('');
    }
  };
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  );
};

效果说明

这段代码实现了表单提交时调用addTodo方法,将任务文本传递给Reducer处理,通过自定义Hook访问Context中的状态和方法,无需手动传递props。

逐句分析

  1. const { addTodo } = useTodoContext();

    • Context中提取addTodo方法,直接调用即可触发状态更新。
  2. addTodo(text.trim())

    • 提取用户输入的文本,调用addTodo方法,派发Add_Todo类型的action
  3. setText('')

    • 清空输入框,重置表单状态。

3.5 渲染Todo列表

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

const TodoList = () => {
  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>
          <button onClick={() => removeTodo(todo.id)}>Remove</button>
        </li>
      ))}
    </ul>
  );
};

效果说明

  • 这段代码用于渲染Todos列表,提供切换状态和删除操作,并通过useTodoContext()直接访问Context中的todos列表和操作方法。

逐句分析

  1. const { todos, toggleTodo, removeTodo } = useTodoContext();

    • Context中提取Todos列表和操作方法,无需父组件传递。
  2. onClick={() => toggleTodo(todo.id)}

    • 点击任务时调用toggleTodo,派发Toggle_Todo类型的action,切换任务状态。
  3. onClick={() => removeTodo(todo.id)}

    • 点击删除按钮时调用removeTodo,派发Remove_Todo类型的action,删除任务。
  4. style={{ textDecoration: todo.done ? 'line-through' : 'none' }}

    • 根据任务的done状态动态调整样式,已完成任务显示删除线。

四、总结

通过ContextReducer的组合,我们实现了以下目标:

  1. 解耦状态管理:将状态逻辑与组件渲染分离,提升代码可维护性。
  2. 跨层级通信:无需手动传递props,通过Context直接访问共享状态。
  3. 可预测的状态更新:通过Reducer集中管理状态变更逻辑,确保行为一致。

这种模式特别适合中大型应用,既能避免“prop drilling”,又能保持状态更新的清晰可读性。在实际开发中,可以进一步结合自定义Hook和模块化设计,构建更复杂的业务逻辑。


附录:完整代码

 App.jsx

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

function App() {
const todosHook = useTodos()  

  return (
    <TodoContext.Provider value={todosHook}>
     <h1>Todo App</h1>
     <AddTodo></AddTodo>
     <TodoList></TodoList>
    </TodoContext.Provider>
  )
}

export default App

AddTodo.jsx

//components\AddTodo.jsx
import {useTodoContext} from '../hooks/useTodoContext'
import {useState } from 'react'
 
const AddTodo =()=>{
    const [text ,setText] = useState('')
    const {addTodo} =useTodoContext() 
    const handleSubmit = (e)=>{
        e.preventDefault()
        if(text.trim()){
            addTodo(text.trim())
            setText("")
        }
    }
    
    return(
        <>
        <form onSubmit={handleSubmit}>
            <input type="text" value={text} onChange={e => setText(e.target.value)} style={{margin : '10px'}}/>
            <button type="submit">添加 </button>
        </form>
        </>
    )
}

export default AddTodo

TodoList.jsx

//components\TodoList.jsx
import { useTodoContext } from '../hooks/useTodoContext'

const TodoList = () => {
    const {
        todos,
        toggleTodo,
        removeTodo
    } = useTodoContext()
    
    return (
        <>
            {
                todos.map(todo => (
                    <li key={todo.id}>
                        <span onClick={() => toggleTodo(todo.id)}
                            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
                            {todo.text}
                        </span>
                        <button onClick={() => removeTodo(todo.id)} style={{ 'margin-left': '10px', 'margin-top': '10px' }}>删除</button>
                    </li>
                ))
            }
        </>
    )
}

export default TodoList

 useTodoContext.js

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

export function useTodoContext(){
    return useContext(TodoContext);
}

 useTodo.js

// hooks/useTodo.js
import { useReducer } from "react";
import todoReducer from "../reducers/todoReaducer";

export function useTodos(inital=[]){
    const [todos,dispatch] = useReducer(todoReducer,inital);
    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,
    }
}

 todoReducer.js

// reducers/todoReducer.js
function todoReducer(state, action) {
    switch (action.type) {
        case 'Add_Todo':
            return [...state,{
                id: Date.now(),
                text: action.text,
                done: false,
            }];
        case 'Toggle_Todo':
            return state.map(todo => 
                todo.id === action.id?{
                    ...todo, done:!todo.done
                }:todo)
        case 'Remove_Todo':
            return state.filter(todo => todo.id !== action.id)
        default:
            return state;
    }
}
export default todoReducer;

TodoContext.js

// TodoContext.js
import { createContext } from 'react';
export const TodoContext = createContext();