【React State】告别 useState
滥用:何时应该选择 useReducer
所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【React 性能】性能优化第一课:搞懂 React.memo
, useCallback
, useMemo
作者: 码力无边
✨ 引言:你的组件状态,是不是一团“意大利面”?
嘿,各位在 React Hooks 的世界里构建奇妙应用的前端道友们,我是码力无边!
自从 React Hooks 横空出世,useState
就如同我们修仙路上的第一把“新手木剑”。它简单、直接、锋利,几乎能解决我们遇到的 80% 的状态管理问题。
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
三下五除二,几个 useState
一摆,组件的状态就管理起来了。对于简单的组件,这套“剑法”行云流水,好不快哉!
但随着我们修为的加深,要面对的“妖魔”(业务逻辑)也越来越强大。你的组件状态不再是简单的数字或字符串,而是一个包含了多个子值、彼此之间还可能有关联的复杂对象。此时,如果你还固执地使用“新手木剑”,你的组件很快就会变成这样:
function ComplexForm() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [step, setStep] = useState(1);
const handleSubmit = () => {
setIsLoading(true);
setError(null);
// ... 发送请求
// 成功后...
setIsLoading(false);
setStep(2);
// 失败后...
setIsLoading(false);
setError('请求失败...');
};
// ...
}
看到问题了吗?
- 状态“碎片化”:多个看似独立、实则相关的状态被分散在各处。
- 更新逻辑“意大利面化”:一个
handleSubmit
函数里,包含了多个setXXX
调用。这些状态更新逻辑散落在组件的各个事件处理函数中,难以追踪和维护。当逻辑变得更复杂时(比如,根据错误类型显示不同信息),这里的代码会迅速膨胀成一坨难以理解的“意大利面”。
当你的“新手木剑”已经舞不动这日益复杂的“剑招”时,是时候请出 React 为我们准备的另一把“上古神兵”了——useReducer
。
useReducer
就像是一本高级“剑谱”,它要求你将**“如何更新状态”的逻辑**(剑招)从组件中分离出来,让状态的流转变得清晰、可预测、易于测试。
今天,码力无边就将带你深入这本“剑谱”,让你明白 useReducer
并非 useState
的替代品,而是处理复杂状态场景的“最优解”。你将学会何时应该放下手中的“木剑”,并自信地挥舞起 useReducer
这把神兵。
一、useReducer
的“内功心法”:三要素
初见 useReducer
,你可能会觉得它比 useState
繁琐。但它的“繁琐”恰恰是其强大之所在。它由三个核心要素构成:
1. State (状态):和 useState
一样,是你需要管理的数据。但通常,它是一个包含了多个子值的对象。
2. Action (动作):一个描述“发生了什么”的普通 JavaScript 对象。按照约定,它通常有一个 type
属性(字符串,描述动作类型)和一个可选的 payload
属性(携带的数据)。
{ type: 'INCREMENT' }
{ type: 'UPDATE_FIELD', payload: { field: 'username', value: '码力无边' } }
3. Reducer (归纳函数):这是一个纯函数,是 useReducer
的灵魂。它接收当前 state
和一个 action
作为参数,然后返回一个新的 state
。
(state, action) => newState
- 它的作用就像一个“状态管理员”,根据收到的“指令” (
action
),来决定如何基于旧状态 (state
) 计算出新状态。它只负责计算,不产生副作用(比如 API 请求)。
useReducer
的基本用法
让我们来看一个经典的计数器例子,感受一下它的运作流程:
import React, { useReducer } from 'react';
// 3. 定义 Reducer 函数
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
throw new Error();
}
};
function Counter() {
// 1. 定义初始 State
const initialState = { count: 0 };
// useReducer 接收 reducer 函数和初始 state
// 返回当前 state 和一个 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
{/* 2. 通过 dispatch 发送 Action */}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</>
);
}
流程解析:
- 用户点击 “+” 按钮。
onClick
事件被触发,调用dispatch({ type: 'increment' })
。dispatch
函数将这个action
对象“派发”给reducer
函数。- React 调用
reducer(currentState, { type: 'increment' })
。 reducer
内部的switch
语句匹配到'increment'
,执行return { count: state.count + 1 }
。useReducer
接收到reducer
返回的新 state,用它来更新组件的state
,并触发重新渲染。
二、何时拔出 useReducer
这把神兵?
现在你已经了解了 useReducer
的基本招式。但一个高手,重在懂得“时机”。什么时候,我们应该果断放弃 useState
,转而使用 useReducer
呢?
时机一:当 State 是一个复杂的对象或数组时
当你的 state 包含了多个相互关联的子值时,useReducer
可以让你将这些状态聚合管理,避免声明一大堆 useState
。
让我们重构文章开头的那个复杂表单:
改造前 (useState 版本):
- 状态分散,更新逻辑混乱。
改造后 (useReducer 版本):
const initialState = {
username: '',
email: '',
password: '',
isLoading: false,
error: null,
step: 1,
};
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.payload.field]: action.payload.value };
case 'SUBMIT_START':
return { ...state, isLoading: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...state, isLoading: false, step: 2 };
case 'SUBMIT_FAILURE':
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
}
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleFieldChange = (field, value) => {
dispatch({ type: 'UPDATE_FIELD', payload: { field, value } });
};
const handleSubmit = () => {
dispatch({ type: 'SUBMIT_START' });
api.submitForm({ /* ... */ })
.then(() => dispatch({ type: 'SUBMIT_SUCCESS' }))
.catch(err => dispatch({ type: 'SUBMIT_FAILURE', payload: err.message }));
};
// ... render logic using state.username, state.isLoading etc.
}
优势体现:
- 状态聚合:所有表单相关的状态都集中在
state
对象中,一目了然。 - 逻辑集中:所有状态更新的逻辑都收敛到了
formReducer
内部。handleSubmit
函数的职责变得非常清晰:只负责触发“开始”、“成功”、“失败”这些高级别的“意图”,而不关心具体的state
如何变化。 - 可读性增强:读
dispatch({ type: 'SUBMIT_SUCCESS' })
远比读一堆setIsLoading(false); setStep(2);
要清晰得多。
时机二:当下一个 State 依赖于前一个 State 时
我们知道,useState
的 setState
函数也可以接收一个函数来安全地更新依赖于前一个 state 的值,比如 setCount(c => c + 1)
。
useReducer
在这方面做得更彻底。因为 reducer
函数总是能接收到最新的 state
,所以处理复杂的、依赖于前序状态的逻辑转换变得非常自然和安全。
场景: 实现一个带“撤销 (Undo)”功能的计数器。
const initialState = {
count: 0,
history: [],
};
function undoableReducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
history: [...state.history, state.count], // 把旧的 count 存入历史
};
case 'undo':
if (state.history.length === 0) return state; // 没有历史记录
const previousCount = state.history[state.history.length - 1];
return {
...state,
count: previousCount,
history: state.history.slice(0, -1), // 移除最后一条历史
};
default:
return state;
}
}
用 useState
来实现这个逻辑,你需要维护两个 useState
(count
和 history
),并且在更新 count
的同时,还要小心翼翼地同步更新 history
,非常容易出错。而 useReducer
将这个原子性的、依赖性的更新封装得天衣无缝。
时机三:当你想优化深层组件的性能时
这是一个非常重要但经常被忽略的优势。
场景: 一个深层嵌套的组件树,顶层组件 App
有一个状态,而最底层的组件 DeepChild
需要修改这个状态。
useState
的方案:
App
需要把 setState
函数一层一层地通过 props 传递下去 (App -> Middle -> DeepChild
)。
function App() {
const [theme, setTheme] = useState('light');
return <Middle setTheme={setTheme} />;
}
function Middle({ setTheme }) { return <DeepChild setTheme={setTheme} />; }
function DeepChild({ setTheme }) { return <button onClick={() => setTheme('dark')}>Change</button>; }
问题在于,setTheme
函数的引用地址在每次 App
渲染时可能会变(取决于 React 的实现细节,但我们不应依赖它),这会导致 Middle
和 DeepChild
即使被 React.memo
包裹,也可能因为 setTheme
prop 的变化而重新渲染。
useReducer
的方案:
useReducer
返回的 dispatch
函数,React 保证其引用地址在组件的整个生命周期内是稳定的。
const reducer = (state, action) => { /* ... */ };
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch 的引用是稳定的!
return <Middle dispatch={dispatch} />;
}
function Middle({ dispatch }) { return <DeepChild dispatch={dispatch} />; }
function DeepChild({ dispatch }) { return <button onClick={() => dispatch({ type: 'CHANGE_THEME' })}>Change</button>; }
现在,你可以放心地用 React.memo
包裹 Middle
和 DeepChild
,因为传递给它们的 dispatch
prop 永远不会变,它们不会因为父组件的渲染而跟着不必要地渲染。
当然,这个问题也可以用 useCallback
来包装 setTheme
函数解决,但 useReducer
提供了一种更“原生”、更具结构化的解决方案。当结合 Context
API 进行全局状态管理时,这个优势会更加明显。
写在最后:useReducer
是一种思想的转变
useState
和 useReducer
并非“非黑即白”的对立关系,它们是 React 工具箱中针对不同场景的两种工具。
useState
是处理局部、简单、独立状态的“手枪”,轻便快捷。useReducer
是处理复杂、关联、跨组件状态的“狙击枪”,精准、强大、有章法。
从 useState
转向 useReducer
,不仅仅是 API 的替换,更是一种思想的转变:
- 从命令式(“我要把
isLoading
设为true
,把error
设为null
”)到声明式(“我要发起一个‘提交开始’的动作”)。 - 从将状态更新逻辑与组件的视图逻辑耦合到将它们彻底分离。
掌握 useReducer
,意味着你开始以一种更宏观、更具架构性的视角来思考组件的状态管理。你的组件将变得更具可读性、可预测性、可测试性,也更能从容地应对未来日益复杂的业务需求。这,就是从“会用 React”到“精通 React”的一次重要跃迁。
专栏预告与互动:
我们已经掌握了 React 核心的状态管理工具。但在实际开发中,我们往往需要封装自己的逻辑,让它们能在多个组件之间复用。这就是自定义 Hooks 的魅力所在。
下一篇,我们将进入 React Hooks 的封装艺术,学习如何编写高质量的自定义 Hooks,将你的重复逻辑抽象成可复用的“超能力”,极大提升你的开发效率和代码质量!
感觉码力无边的“状态管理剑谱”让你茅塞顿开?点赞、收藏、关注,你的每一次支持,都是我绘制下一幅“武功图谱”的灵感源泉!
今日论道: Redux 是一个非常流行的 React 全局状态管理库,它的核心思想(State, Action, Reducer)和
useReducer
几乎如出一辙。你认为在只有中小型状态管理需求的场景下,useReducer
+Context API
的组合能否完全替代 Redux?为什么?在评论区分享你的看法,我们一起探讨!