React状态管理进阶:用useReducer + useContext打造优雅的全局状态

94 阅读5分钟

前言

还在为React的状态管理头疼吗?useState满天飞,props层层传递,状态逻辑散落各处?今天带你用React原生的Hook组合拳——useReducer + useContext,打造一个既优雅又实用的全局状态管理方案。

不需要Redux,不需要Zustand,就用React自带的这两个Hook,我们就能实现一个功能完整的Todo应用状态管理系统。

项目结构一览

在开始之前,我们先看看整个项目的文件结构。这样能让你对项目有个全局的认识:

todo-app/
├── public/
│   └── index.html
├── src/
│   ├── components/           # UI组件目录
│   │   ├── AddTodo.jsx      # 添加待办事项组件
│   │   └── TodoList.jsx     # 待办事项列表组件
│   ├── hooks/               # 自定义Hook目录
│   │   ├── useTodos.js      # Todo状态管理Hook
│   │   └── useTodoContext.js # Context消费Hook
│   ├── reducers/            # Reducer目录
│   │   └── todoReducer.js   # Todo状态变更逻辑
│   ├── App.jsx              # 根组件
│   ├── App.css              # 应用样式
│   ├── TodoContext.js       # 全局Context定义
│   ├── main.jsx            # 应用入口
│   └── index.css           # 全局样式
└── package.json

这个结构看起来是不是很清爽?我们按照功能模块来组织代码:

  • components/ 存放UI组件
  • hooks/ 存放自定义Hook
  • reducers/ 存放状态管理逻辑
  • 根目录放应用的主要文件

output_very_clear.gif

核心思路:Hook组合的艺术

在深入代码之前,先理解一下我们的整体架构:

  • useReducer:负责响应式状态管理,用纯函数定义状态变更规则
  • useContext:负责跨组件通信,让状态在组件树中自由流动
  • 自定义Hook:将状态逻辑与UI渲染分离,让组件专注于显示

这三者组合起来,就是一个完整的全局应用级别响应式状态管理方案。

第一步:定义状态变更规则

首先我们需要一个reducer来管理Todo的状态变更。这个函数必须是纯函数,相同输入永远产生相同输出:

// 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;

这个reducer定义了三种操作:

  • ADD_TODO:新增待办事项,用时间戳作为唯一ID
  • TOGGLE_TODO:切换完成状态,用map遍历找到对应项目
  • REMOVE_TODO:删除待办事项,用filter过滤掉指定ID

第二步:创建上下文通信管道

Context就像是组件间的一条高速公路,让数据可以跨越组件层级自由传递:

// TodoContext.js
import {
     createContext,
     useContext
     } from "react";

// 创建上下文
export const TodoContext = createContext(null);

第三步:封装自定义Hook

这是整个方案的核心,我们把状态逻辑都封装在这个自定义Hook里:

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

// 使用参数默认值,支持初始化数据
export function useTodos(inital = []){
    const [todos, dispatch] = useReducer(todoReducer, inital);
    
    // 封装dispatch操作,让组件使用更简单
    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
    }
}

export default useTodos;

这个Hook做了几件关键的事:

  1. 用useReducer管理状态
  2. 封装了dispatch操作,让API更友好
  3. 返回一个包含状态和方法的对象

第四步:创建Context消费Hook

为了让其他组件更方便地使用Context,我们再封装一个Hook:

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

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

第五步:在根组件中提供状态

现在在App组件中把所有东西串联起来:

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

function App() {
  // 创建todos状态管理实例
  const todosHook = useTodos();
  
  return (
    // 通过Provider将状态注入到组件树
    <TodoContext.Provider value={todosHook}>
      <h1>Todo App</h1>
      <AddTodo />
      <TodoList />
    </TodoContext.Provider>
  )
}

export default App

第六步:在子组件中消费状态

添加Todo组件

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

const AddTodo = () =>  {
    // 私有状态,管理输入框
    const [text, setText] = useState('');
    // 从Context获取全局状态操作方法
    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)} 
              placeholder="输入待办事项..."
            />
            <button type="submit">Add</button>
        </form>
    )
}

export default AddTodo;

Todo列表组件

// TodoList.jsx
import { useTodoContext } from "../hooks/useTodoContext";

const TodoList = () => {
    // 从Context获取状态和操作方法
    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',
                            cursor: 'pointer'
                        }}
                    >
                        {todo.text}
                    </span>
                    <button onClick={() => removeTodo(todo.id)}>
                        Remove
                    </button>
                </li>
            ))}
        </ul>
    )
}

export default TodoList;

方案优势分析

1. 响应式状态管理

useReducer提供了强大的状态管理能力:

  • 状态变更逻辑集中管理
  • 纯函数保证状态变更的可预测性
  • 复杂状态逻辑处理更加清晰

2. 跨层级通信

useContext解决了props drilling问题:

  • 状态可以跨越任意组件层级传递
  • 不需要在中间组件中传递无关的props
  • 组件结构更加清晰

3. 关注点分离

自定义Hook实现了完美的关注点分离:

  • 组件专注于UI渲染
  • Hook专注于状态管理
  • 逻辑复用变得简单

4. 类型友好

这套方案对TypeScript非常友好,可以很容易地添加类型定义。

扩展思考

这套方案不仅适用于Todo应用,还可以扩展到:

  • 主题管理:light/dark主题切换
  • 用户登录:用户信息、登录状态管理
  • 购物车:商品添加、删除、数量管理
  • 表单状态:复杂表单的状态管理

只需要:

  1. 定义对应的reducer
  2. 创建对应的Context
  3. 封装自定义Hook

小结

通过useReducer + useContext的组合,我们用React原生的能力实现了一个功能完整的状态管理方案。这套方案既保持了代码的简洁性,又提供了足够的灵活性。

相比于引入第三方状态管理库,这种方案有几个明显优势:

  • 零依赖,基于React原生API
  • 学习成本低,不需要额外的概念
  • 体积小,不会增加bundle大小
  • 调试友好,可以直接使用React DevTools

当然,如果你的应用状态管理需求非常复杂,可能还是需要考虑Redux、Zustand等专业的状态管理库。但对于大多数中小型应用,这套原生方案已经完全够用了。

最后想说,技术选型没有银弹,适合的才是最好的。React Hook的组合威力远比我们想象的强大,多尝试、多思考,你会发现更多有趣的用法!