2.使用 useState 的函数式更新方式能带来哪些好处? 副本
查看飞书原文 ↗4,330 字符
useState :为什么函数式更新更可靠?
在日常的 React 开发中,useState 是我们最亲密的伙伴之一。我们通常会这样使用它:
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1); // 直接传入一个新值
};
这种 setCount(newValue) 的方式直观且易于理解。然而,useState 还提供了另一种看似稍显繁琐的更新模式——函数式更新:
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1); // 传入一个函数
};
既然已经有了直接更新的方式,为什么 React 还要提供函数式更新呢?它仅仅是一种语法偏好,还是背后蕴含着更深层的原因?
答案是后者。在某些特定场景下,函数式更新不仅是“更好”的选择,甚至是“唯一正确”的选择。它能帮助我们有效规避一类因 State 闭包陷阱(Stale Closure)导致的常见 Bug。
问题浮现:当 State 更新不符合预期
让我们从一个简单的反例开始。假设我们需要在一次交互中,连续两次将 count 的值加一。很多人会很自然地写出以下代码:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleTripleIncrement = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleTripleIncrement}>Increment by 3</button>
</div>
);
}
直觉上,每次点击按钮,count 应该会增加 3。但实际运行后你会发现,count 每次只增加了 1。
这背后的本质原因是:setCount 的更新是异步的,并且 React 会对短时间内连续的更新进行批处理(Batching)。
在 handleTripleIncrement 函数的执行上下文中,count 的值始终是当次渲染时的快照。当我们连续三次调用 setCount(count + 1) 时,对于 React 而言,它收到的指令是:
1.
setCount(0 + 1)
2.
setCount(0 + 1)
3.
setCount(0 + 1)
由于这三次更新被批处理,最终的结果是将 count 更新为 1,而不是我们期望的 3。同样的问题也会出现在异步操作中,例如在一个 setTimeout 的回调里去更新一个已经被外部改变的 State。
解决方案:使用函数获取最新 State
为了解决这个问题,函数式更新应运而生。当我们向 set 函数传递一个函数时,React 会保证这个函数的入参(我们通常命名为 prevState 或 prevCount)**永远是当前最新的 State 值**。
React 会将这些更新函数维护在一个队列中,并在下一次渲染前依次执行,确保每一次计算都是基于前一次更新后的结果。
现在,我们用函数式更新来重构上面的例子:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleTripleIncrement = () => {
// 每次都基于最新的 state 进行计算
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleTripleIncrement}>Increment by 3</button>
</div>
);
}
这一次,代码的行为就完全符合预期了。每次点击,count 都会正确地增加 3。React 的处理流程变成了:
1.
将 prevCount => prevCount + 1 推入更新队列。
2.
再次将 prevCount => prevCount + 1 推入队列。
3.
第三次推入队列。
4.
在处理阶段,取出第一个函数,执行 0 + 1,结果为 1。
5.
取出第二个函数,此时 prevCount 是 1,执行 1 + 1,结果为 2。
6.
取出第三个函数,此时 prevCount 是 2,执行 2 + 1,结果为 3。
7.
最终,将 count 的状态更新为 3。
函数式更新的核心优势
综上所述,我们可以总结出函数式更新带来的几个关键好处:
1.
保证 State 更新的准确性:这是最核心的优势。它能确保我们的更新逻辑始终基于最新的 State,从而彻底避免因 State 闭包陷阱或批处理带来的数据不一致问题。
2.
代码逻辑更具可预测性:当看到 setCount(c => c + 1) 这样的代码时,我们能立刻明白开发者的意图是“在当前值的基础上加一”。这种更新方式是自包含的,不依赖外部的变量,降低了代码的耦合度和出错的可能性。
3.
优化自定义 Hook 的设计:在编写自定义 Hook 时,有时我们只需要向外暴露 State 的更新能力,而不需要暴露 State 本身。函数式更新使得我们可以只传递 set 函数,而无需同时传递 State 值,从而让 Hook 的接口设计更加简洁和安全。
最佳实践:我们该如何选择?
那么,在实际开发中,我们应该遵循什么样的原则呢?一个简单有效的经验法则是:
•
如果新的 State 依赖于旧的 State,请始终使用函数式更新。
◦
计数器:setCount(prev => prev + 1)
◦
布尔值切换:setToggle(prev => !prev)
◦
数组操作:setItems(prev => [...prev, newItem])
•
如果新的 State 与旧的 State 完全无关,可以直接传入新值。
◦
表单输入:setName(e.target.value)
◦
重置状态:setCount(0)
结论
函数式更新并非只是一个可有可无的语法糖,而是 React 为我们提供的一个用于编写更健壮、更可预测组件的强大工具。养成在“基于旧值计算新值”的场景下使用函数式更新的习惯,将大大减少我们应用中潜在的 Bug,提升代码的整体质量。
正在生成 AI 速览...
本文讨论了 React 中 useState 的两种更新方式,重点讲解了函数式更新的作用、优势及使用原则,关键要点包括:
1.
直接传值更新的问题:React 会对短时间内的 useState 更新做批处理,直接传入新值的方式会因 State 闭包陷阱,导致连续更新、异步更新时结果不符合预期,比如连续三次调用 setCount(count + 1) 最终只会让 count 加 1 而非 3。
2.
函数式更新的核心逻辑:向 set 函数传入更新函数时,React 会将其存入队列,渲染前依次执行,入参永远是最新的 State 值,确保每次计算基于前一次更新结果。
3.
函数式更新的核心优势:一是保证 State 更新准确,彻底规避闭包陷阱、批处理带来的数据不一致问题;二是代码逻辑更可预测,不依赖外部变量,降低耦合与出错概率;三是可优化自定义 Hook 设计,让接口更简洁安全。
4.
函数式更新的使用原则:新 State 依赖旧 State 时必须使用,比如计数器累加、布尔值切换、数组追加等场景;新 State 和旧值完全无关时可直接传新值,比如表单赋值、状态重置。
5.
函数式更新的定位:并非语法糖,是 React 提供的编写健壮组件的工具,养成对应场景的使用习惯可大幅减少潜在 Bug,提升代码质量。
内容由 AI 生成
本文讨论了 React 中 useState 的两种更新方式,重点讲解了函数式更新的作用、优势及使用原则,关键要点包括:
1.
直接传值更新的问题:React 会对短时间内的 useState 更新做批处理,直接传入新值的方式会因 State 闭包陷阱,导致连续更新、异步更新时结果不符合预期,比如连续三次调用 setCount(count + 1) 最终只会让 count 加 1 而非 3。
2.
函数式更新的核心逻辑:向 set 函数传入更新函数时,React 会将其存入队列,渲染前依次执行,入参永远是最新的 State 值,确保每次计算基于前一次更新结果。
3.
函数式更新的核心优势:一是保证 State 更新准确,彻底规避闭包陷阱、批处理带来的数据不一致问题;二是代码逻辑更可预测,不依赖外部变量,降低耦合与出错概率;三是可优化自定义 Hook 设计,让接口更简洁安全。
4.
函数式更新的使用原则:新 State 依赖旧 State 时必须使用,比如计数器累加、布尔值切换、数组追加等场景;新 State 和旧值完全无关时可直接传新值,比如表单赋值、状态重置。
5.
函数式更新的定位:并非语法糖,是 React 提供的编写健壮组件的工具,养成对应场景的使用习惯可大幅减少潜在 Bug,提升代码质量。