从计数器到TodoList:React的Reducer与Context如何拯救你的状态管理?

99 阅读6分钟

嗨,各位前端工友!👋 最近是不是又被React的状态管理搞得头大? useState写多了觉得散乱,用Redux又嫌太重?今天咱们就聊聊两个「轻量级但超实用」的API—— ReducerContext ,看看它们是如何联手解决React开发中的「状态混乱」和「传参地狱」的。

先上结论: Reducer 管「怎么改」,Context 管「怎么传」 ,二者搭配使用,堪称中小型项目的状态管理黄金搭档!

什么是Context

Context 是 React 提供的跨组件数据共享方案,用于解决多层级组件间的 props 传递问题(prop drilling)。

核心作用

  • 数据共享:允许父组件向其所有后代组件广播数据,无需手动逐层传递 props
  • 简化状态传递:尤其适合主题设置、用户信息等全局状态

使用步骤

  1. 创建 Context:const MyContext = createContext(defaultValue)
  2. 提供数据:使用 <MyContext.Provider value={共享数据}> 包裹组件树
  3. 消费数据:通过 useContext(MyContext) 在子组件中获取数据

什么是Reducer

Reducer 是一种状态管理模式,通过纯函数统一处理状态变更逻辑,配合 useReducer Hook 使用。

核心作用

  • 集中管理复杂状态逻辑:将多个相关状态的修改逻辑整合到单一函数
  • 可预测的状态变更:通过 action 类型明确状态修改意图,便于调试
  • 处理复杂状态依赖:当多个状态相互关联或需要复杂计算时尤为适用

使用步骤

  1. 定义 reducer 函数: (state, action) => newState
  2. 初始化状态: const [state, dispatch] = useReducer(reducer, initialState)
  3. 触发状态更新: dispatch({ type: 'ACTION_TYPE', payload: 数据 })

没有它们,我们会遇到什么麻烦?

假设我们要写一个TodoList:

  • 组件A(AddTodo)需要添加任务 → 修改状态
  • 组件B(TodoList)需要展示任务 → 读取状态
  • 组件C(Footer)需要显示任务数量 → 读取状态 如果只用useState + props传递:
// 伪代码:props层层传递的噩梦
function App() {
  const [todos, setTodos] = useState([])
  return (
    <div>
      <AddTodo setTodos={setTodos} />
      <TodoList todos={todos} setTodos={setTodos} />
      <Footer todos={todos} />
    </div>
  )
}

当组件层级变深(比如TodoList里嵌套Item组件),props要传3层以上,改一处状态要动N个地方——这就是prop drilling( props钻取)。更要命的是,如果状态逻辑复杂(比如添加/删除/修改/筛选),useState的setter函数会散落在各个事件处理中,调试时根本找不到状态是在哪改的!

Reducer:让状态修改「有法可依」

Reducer的核心作用:把「状态修改逻辑」集中管理 ,就像给状态变化制定了「宪法」,所有修改必须按规则来。

先看下面的计数器实现:

// 1. 定义初始状态
const initialState = { count: 0, theme: 'light' }

// 2. 编写Reducer函数(状态修改的「宪法」)
function reducer(state, action) {
  switch (action.type) {
    case 'increment': // 增加计数
      return { ...state, count: state.count + 1 }
    case 'decrease': // 减少计数
      return { ...state, count: state.count - 1 }
    case 'changeTheme': // 切换主题
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' }
    default: return state
  }
}

// 3. 在组件中使用useReducer
function App() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <div>
      <h1>计数:{state.count}</h1>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'changeTheme' })}>切换主题</button>
    </div>
  )
}

Reducer解决了什么问题?

  1. 逻辑集中化 :所有状态修改逻辑都在reducer函数里,想知道 count 怎么变的?搜reducer函数就行,不用满组件找setCount
  2. 可预测性 :修改状态必须通过 dispatch(action)actiontype 字段(比如'increment')就是修改的「操作名」,调试时Redux DevTools能清晰显示每一步状态变化。
  3. 复杂状态管理 :当状态有多个子值(比如{count, theme, user})或修改逻辑相互依赖时,reducer 比 useState 的多个 setter 函数优雅10倍!

Context:让状态传递「一步到位」

Reducer 解决了「怎么改」,但状态还得靠props传递?别急, Context的作用就是:跨组件共享状态,彻底消灭prop drilling

看看下面这个项目是怎么做的。这是一个完整的 TodoList,用 Context 把状态「广播」给所有组件:

1. 创建Context(相当于「状态广播电台」)

import { createContext } from "react";
// 创建一个Context,默认值为null
export const TodoContext = createContext(null);

2. 用Provider「发射」状态(相当于「电台发射塔」)

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

function App() {
  // 用useReducer封装状态和修改方法(这里useTodos内部调用了useReducer)
  const todosHook = useTodos([]);
  return (
    {/* Provider包裹需要共享状态的组件树 */}
    <TodoContext.Provider value={todosHook}>
      <h1>Todo App</h1>
      <AddTodo /> {/* 无需传props! */}
      <TodoList /> {/* 无需传props! */}
    </TodoContext.Provider>
  )
}

3. 用useContext「接收」状态(相当于「收音机」)

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

export function useTodoContext() {
  // 直接从Context获取状态,无需通过props
  return useContext(TodoContext)
}

现在,AddTodoTodoList 组件可以直接通过 useTodoContext() 拿到todos和修改方法,比如AddTodo添加任务:

const { addTodo } = useTodoContext()
const handleSubmit = (e) =>{
  e.preventDefault();
  if(text.trim()){
    addTodo(text.trim()) // 直接调用Context提供的方法
    setText('')
  }
}

以及 TodoList通过调用 useTodoContext hook取得数据并展示

import { useTodoContext } from '../hooks/useTodoContext'
...
const TodoList = ()=>{
  const {
    todos,
    toggleTodo,
    removeTodo
  } = useTodoContext()
  

Context解决了什么问题?

  • 跨组件传参 :无论组件嵌套多深,只要在Provider内部,就能直接获取状态,不用再写 props={props} 这种废话。
  • 状态共享 :多个组件需要用同一个状态时(比如用户信息、主题设置),Context比全局变量更安全(React控制更新)。

Reducer + Context:强强联手的正确姿势

单独用Reducer只能管一个组件的状态,单独用Context虽然能传状态但状态修改逻辑还是散的。 把二者结合 ,才是中小项目的最优解!

经典搭配步骤:

  1. 用Reducer管理状态逻辑 :写一个 reducer 函数,定义所有状态修改规则(如 todoReducer)。
  2. 用useReducer创建状态 :在顶层组件(如App)中调用 useReducer,得到 [state, dispatch]
  3. 用Context传递状态和dispatch :通过 Context.Providerstatedispatch 传给所有子组件。
  4. 子组件按需取用 :用 useContext 获取 state(读)和 dispatch(改),实现「读写分离」。

看看这个项目最佳实践—— useTodos hook封装了reducer逻辑:

export function useTodos(initial = []){
  const [todos, dispatch] = useReducer(todoReducer, initial)
  // 把dispatch包装成具体方法(addTodo/toggleTodo),子组件无需知道dispatch细节
  const addTodo= text =>{dispatch({type: 'ADD_TODO', text})}
  const toggleTodo= id =>{dispatch({type: 'TOGGLE_TODO', id})}
  return { todos, addTodo, toggleTodo }
}

这样一来,子组件拿到的是 addTodo 这种直观的方法,而不是底层的 dispatch ,代码可读性直接拉满!

什么时候该用它们?

场景用useState用Reducer用ContextReducer+Context
简单状态(如开关、计数器)✅ 首选❌ 小题大做❌ 没必要❌ 过度设计
复杂状态(多子值、多修改逻辑)❌ 散乱✅ 首选❌ 只管传不管改❓ 单组件可用
跨组件共享简单状态❌ prop drilling❌ 不解决传参✅ 首选❌ 没必要
跨组件共享复杂状态❌ 灾难❌ 只管改不管传❌ 状态修改散乱✅ 黄金搭档

总结:别再滥用Redux了!

很多同学一上来就用Redux,其实大部分中小项目根本用不上。 Reducer+Context已经能解决80%的状态管理问题 :

  • Reducer :让状态修改「有迹可循」,适合复杂状态逻辑。
  • Context :让状态传递「直达目标」,适合跨组件共享。
  • 二者结合:既解决了「怎么改」,又解决了「怎么传」,代码干净又好维护!

Context像快递小哥,把状态包送到各个组件家门口;Reducer像交通指挥官,给状态变更制定红绿灯规则。俩兄弟联手,React状态管理从此告别「堵车」和「丢件」!