👋 哈喽,各位掘金的朋友们!我是你们的老朋友,今天想和大家聊一个在 React 开发中既经典又实用的话prehensive话题:状态管理。
当我们的应用开始变得复杂,组件层级越来越深时,你是否也曾被 props drilling(属性逐级传递)搞得头昏脑ota?或者为了管理一些复杂的状态逻辑,在 useState
的世界里挣扎?
别担心,今天我将带大家返璞归真,利用 React 内置的 Hooks------useReducer
和 useContext
,手把手教你打造一个轻量、高效且极度优雅的全局状态管理方案。看完这篇,你会发现,原来不依赖 Redux、MobX 等外部库,我们也能把状态管理玩得明明白白!🚀
核心思想:专业分工,各司其职
在开始敲代码之前,我们先来理解一下这个方案的核心思想,这其实就是一种"专业分工"的理念:
-
useReducer
:状态逻辑的"大管家"- 它负责如何管理状态 。对于复杂的状态变更逻辑,
useReducer
提供了比useState
更清晰、更可预测的管理方式。它遵循 Redux 的思想,通过一个纯函数reducer
来规定所有状态变更的"规矩"。 - 每当我们需要改变状态时,不再是直接
setState
,而是dispatch
(派发)一个action
(指令),比如"新增一条待办"、"删除一个任务"。这种方式让状态的每一次变化都有迹可循。
- 它负责如何管理状态 。对于复杂的状态变更逻辑,
-
useContext
:数据通信的"高速公路"- 它负责如何共享状态 。
useContext
的使命就是解决 props drilling 的痛点,它能创建一个全局的"上下文",让任何层级的子组件都能轻松访问到顶层提供的共享数据,无需层层传递。
- 它负责如何共享状态 。
当这两位高手联手时,奇妙的化学反应就发生了:useReducer
管理着我们应用的状态(比如主题、登录信息、待办事项列表),然后通过 useContext
将这些状态和管理状态的方法(dispatch
)高效地"广播"给所有需要它们的组件。这就构成了一个完整的、应用级别的状态管理闭环。
实战演练:从零打造一个 Todo List
理论说完了,我们立刻进入实战环节!下面,我们将以一个经典的 Todo List
应用为例,一步步拆解这个模式的实现。
第一步:创建"上下文" - TodoContext.js
首先,我们需要一个"容器"来存放我们共享的状态。这就是 Context
的作用。
javascript
// src/TodoContext.js
import {
createContext,
} from "react";
// 上下文:创建一个空的上下文,像一个等待被填充的篮子
export const TodoContext = createContext(null);
代码很简单,调用 createContext(null)
就创建了一个上下文对象 TodoContext
。它就像一个约定,所有在这个上下文"内部"的组件,未来都有机会读取到 Provider
提供的数据。
第二步:封装状态逻辑 - useTodos.js
(自定义 Hook)
这是我们整个状态管理的核心。我们将所有关于 todos
的状态和操作逻辑都封装在一个自定义 Hook useTodos
中。这正是 README.md
中提到的 "组件 (渲染) + hook(状态)" 的分离思想,让组件更专注于 UI 渲染,而状态逻辑则由 Hook 来处理。
javascript
// src/hooks/useTodos.js
import {
useReducer
} from "react";
import todoReducer from "../reducers/todoReducer";
// es6 参数的默认值
// {todos},key:value 省略
// '' 模板字符串
// ...解构 [] = [] = {}
// 展开运算符,... rest 运算符
export const useTodos = (initial=[]) => {
// useReducer 接收一个 reducer 纯函数和一个初始状态
const [todos, dispatch] = useReducer(todoReducer, initial);
// 创建一系列易于调用的辅助函数,将 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});
// 返回状态和操作状态的方法,形成一个完整的 API
return {
todos,
addTodo,
toggleTodo,
removeTodo
}
};
让我们深度解析一下这段代码:
-
useReducer(todoReducer, initial)
: 这是useReducer
的核心用法。todoReducer
: 这是一个我们未展示但至关重要的纯函数 。它接收当前的state
和一个action
,然后返回一个全新的state
。例如,当收到{type: 'ADD_TODO', text: '学习React'}
这个action
时,它会返回一个包含新待办事项的数组。initial
: 这是我们待办事项列表的初始状态,这里我们用到了 ES6 的参数默认值(initial = [])
,增强了代码的健壮性。[todos, dispatch]
: 这里用到了 ES6 的解构赋值 。useReducer
返回一个数组,第一项是当前的状态todos
,第二项是派发action
的dispatch
函数。
-
封装
dispatch
: 我们没有直接把dispatch
暴露出去,而是创建了addTodo
,toggleTodo
,removeTodo
这样更具语义的函数。这样做的好处是,组件在使用时无需关心action
的具体结构(比如type
是什么),只需要调用addTodo('我的新任务')
即可,大大降低了使用复杂度。 -
返回 API : 最后,这个 Hook 返回一个对象,包含了最新的状态
todos
和所有操作它的方法。这里用到了 ES6 的对象属性简写 ,{ todos }
等同于{ todos: todos }
,让代码更简洁。
第三步:连接一切 - App.jsx
现在我们有了"上下文"和"状态逻辑",是时候在应用的根组件 App.jsx
中将它们连接起来了。
jsx
// src/App.jsx
import './App.css'
import { TodoContext } from './TodoContext'
import { useTodos } from './hooks/useTodos'
import AddTodo from './components/AddTodo.jsx'
import TodoList from './components/TodoList'
function App() {
// 1. 调用自定义 Hook,获取状态和操作 API
const todosHook = useTodos();
return (
// 2. 使用 Provider 将状态和 API "注入" 到上下文中
<TodoContext.Provider value={todosHook}>
<h1>Todo List</h1>
<AddTodo />
<TodoList />
</TodoContext.Provider>
)
}
export default App
这里的关键点是 <TodoContext.Provider value={todosHook}>
:
TodoContext.Provider
是一个特殊的组件,它接收一个value
属性。- 我们将
useTodos()
返回的整个todosHook
对象 ({todos, addTodo, ...}
) 作为value
传递下去。 - 这样一来,所有被
Provider
包裹的子组件(这里是AddTodo
和TodoList
,以及它们内部的任何子孙组件)现在都可以通过useContext
访问到这个value
了。
第四步:在组件中消费状态 - useTodoContext.js
子组件如何方便地拿到 Provider
提供的数据呢?当然可以直接在组件里写 useContext(TodoContext)
,但更优雅的方式是再封装一个自定义 Hook。
javascript
// src/hooks/useTodoContext.js
import {
useContext
} from 'react'
import { TodoContext } from '../TodoContext'
// 这是一个最佳实践:将 useContext 的调用也封装起来
export function useTodoContext() {
return useContext(TodoContext)
}
这个 useTodoContext
Hook 让我们的组件彻底与具体的 TodoContext
对象解耦。组件只需要调用 useTodoContext()
,就能拿到我们需要的一切,而无需关心数据具体是从哪个 Context
来的。
现在,在 TodoList
或 AddTodo
组件中,我们可以像这样轻松地使用它:
jsx
// 伪代码: src/components/TodoList.jsx
import { useTodoContext } from '../hooks/useTodoContext';
function TodoList() {
// 一行代码,轻松获取全局状态和方法
const { todos, toggleTodo, removeTodo } = useTodoContext();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>X</button>
</li>
))}
</ul>
);
}
看,TodoList
组件的代码多么清爽!它不关心 todos
数据从哪里来,也不关心 toggleTodo
具体做了什么,它只负责消费状态和调用方法,完美实现了视图和逻辑的分离。
总结
让我们回顾一下这次的旅程,我们完美地实现了 README.md
中描述的模式:
useReducer
: 负责状态的集中管理和可预见的更新。useContext
: 负责状态在组件树中的高效分发。- 自定义 Hook: 负责封装和复用状态逻辑,让组件代码更简洁。
hook + useContext
: 实现了全局应用级别的响应式状态。hook + useContext + useReducer
: 最终形成了一个完整的、强大的、全局应用级别的响应式状态管理方案。
这个模式不仅能帮你告别繁琐的 props drilling,还能让你的复杂状态逻辑变得井井有条。在你的下一个项目中,当遇到需要跨层级共享状态的场景时,不妨试试这个轻量而强大的 React 原生方案吧!
希望这篇文章能对你有所启发,如果你有任何问题或者更好的想法,欢迎在评论区留言讨论!👇