先记住这个答案
下一状态依赖此前已排队的同一状态时,函数式更新能明确表达这种关系,例如累加次数、切换布尔值或从已有列表生成新列表。异步回调可能捕获旧快照,直接用那个值做加减会覆盖其他任务的更新。updater 接收到的是状态队列推进后的前一值,因此适合组合并发任务的计数变化。它不是所有 setter 都必须使用的形式,也不会让函数闭包中捕获的其他 props、标识或数组自动变成最新值。
- 相对变化用 updater,确定替换值可以直接传
- 异步完成顺序不应破坏任务计数
- updater 只更新状态,不在内部启动请求
两个任务都捕获零时直接减一会发生什么
如果两个保存处理器都根据旧 pending 计算下一值,第二次增加可能覆盖第一次增加,任务完成时又可能从旧值减成负数。问题不是请求必须串行,而是每项任务把自己的相对变化错误表达成了一个过时的绝对结果。
下面把开始和结束分别表达为加一、减一,第二个任务先结束也不会影响最终回到零。finally 负责收尾,catch 负责可见的失败信息,避免只在成功路径减少计数而让界面一直显示还有任务。
import { useState } from 'react';
type Props = { save: () => Promise<void> };
export default function PendingSaves({ save }: Props) {
const [pending, setPending] = useState(0);
const [error, setError] = useState('');
async function start() {
setPending(value => value + 1);
try {
await save();
} catch {
setError('保存失败');
} finally {
setPending(value => value - 1);
}
}
return <>
<button onClick={start}>开始保存</button>
<output>进行中:{pending}</output>
<p role="status">{error}</p>
</>;
}两个未完成保存对应计数二;无论哪一个先结束,计数先变一再变零。示例的错误文案会保留到后续业务明确清除,不表示新的保存自动抹去先前失败。
函数参数只解决对应状态的前一值
setItems(items => items.filter(...)) 中的 items 来自状态队列,但过滤条件若捕获一个旧 projectId,并不会因为写成 updater 就自动更新。应明确本次操作属于哪个项目,把任务身份与输入作为事件或请求契约保存。
对于输入框里已经得到的 event.target.value,直接 setValue(nextValue) 很清楚,因为目标就是用用户输入覆盖。为了统一风格把所有值都包装成忽略参数的函数,没有额外的正确性收益,也容易掩盖相对更新与替换的区别。
关联状态复杂时可以提升到动作模型
任务总数、失败列表和每项任务状态都要协调时,单个计数器可能不足以说明哪些任务仍活跃。可以按任务标识保存状态,并通过 reducer 处理开始、完成、失败和取消,减少多个独立字段相互矛盾的机会。
函数式更新仍应保持纯粹,不能在其中调用 save 或把外部日志当成一次性事件。重复执行 updater 的检查不应再次启动任务。需要取消时也要给出明确的任务终态,避免取消和正常完成各自都把计数减一。
容易答错的地方
- 用了 updater 就不存在任何旧闭包问题
- updater 的参数只提供这一个状态的前一值,函数捕获的其他变量仍遵循 JavaScript 闭包规则;任务目标和其他输入需要单独管理。
- 把请求放进 updater 就能和状态变化保持原子
- 更新函数可能被重新执行,也不负责外部系统事务;请求应放在明确的事件或同步流程中,状态函数只负责计算返回值。
面试官还会怎么问?
同一按钮被连续点击时必须使用 updater 吗?
不同有意点击通常分别处理,不能说每次直接加一都必错。但使用相对更新能表达累加意图,尤其适合一次事件多次更新或异步交错。
取消任务后为什么还可能减两次?
若取消处理和原 Promise 的 finally 都执行同一计数收尾,就会重复减少。应按任务身份保证终态处理一次,而不是只依赖 updater 防止业务重复。
两个状态依赖彼此时只改写 updater 就够吗?
未必,分别更新仍可能把一组业务约束拆散。可以合并相关状态或使用 reducer,让一次动作返回一致的新结构,再分别展示需要的字段。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。