🧠 当你的代码运行太快时才会出现的 React Bug
Source: Dev.to
🧩 问题:“为什么每一行看到的状态都不一样?”
我有一个 PrimeReact DataTable,每行里都有一个按钮。点击按钮时输出如下:
Button 0 clicked → items.length = 1
尽管点击时全局状态显然已经有 3 条数据,每个按钮却表现得好像记住了旧的状态。
🔬 重现步骤
- 向表格中添加 3 行。
- 点击第 1 行的按钮 → 记录 1。
- 点击第 2 行的按钮 → 记录 2。
- 点击第 3 行的按钮 → 记录 3。
每一行似乎都“卡在”了过去的状态。
🤔 为什么这让人如此困惑
- UI 看起来是正确的。
- 状态更新也在工作。
- 没有出现错误或警告。
- 关闭记忆化 (
cellMemo={false}) “解决”了问题,这正是线索所在。
🧠 隐藏的罪魁祸首:记忆化 + 闭包
PrimeReact 的 DataTable 默认开启单元格记忆化 (cellMemo=true) 以提升性能。这意味着只有当特定的记忆键改变时,单元格才会重新渲染。
出了什么问题?
- 已存在的行保持了相同的对象引用。
- 被记忆化的单元格没有重新渲染。
- 事件处理函数 (
onClick) 保留了它们创建时的闭包。
于是每个按钮捕获了创建时的状态快照——这是一种经典的 陈旧闭包 问题,根源在于记忆化,而不是状态逻辑本身。
🧪 最小复现(纯 React)
import React from "react";
const Row = React.memo(
({ item, index, snapshotLength }) => {
const onClick = () => {
console.log(index, snapshotLength);
};
return Click;
},
(prev, next) => prev.item === next.item // ❌ 忽略了 snapshotLength
);
因为在记忆比较函数中忽略了 snapshotLength,行组件从未重新渲染,点击处理函数一直使用陈旧的数据——这正好映射了 DataTable 的 bug。
🛠️ 实际的修复(而不是 hack)
快速变通方案
关闭记忆化会失去大表格的性能提升。
正确的修复方式
在 PrimeReact 中,调整记忆化键,使其在行结构变化时也会改变。
之前
cellMemoProps = { index };
之后
cellMemoProps = { rowIndex };
rowIndex 在添加、删除或重新排序行时会改变,从而让记忆化的单元格在需要时重新渲染。这可以在不牺牲性能的前提下恢复最新的闭包。
✅ 结果
应用修复后:
- Button 0 →
items.length = 3 - Button 1 →
items.length = 3 - Button 2 →
items.length = 3
没有陈旧闭包,没有性能回退,也没有 API 变动。
🤯 额外洞察:为什么 useFieldArray “恰好能工作”
使用 React Hook Form 的 useFieldArray 时从未出现该 bug,因为 useFieldArray 在更新时会创建新的对象引用,天然地使记忆化失效。行组件会自动重新渲染——这并不是魔法,而是对象标识帮了忙。
🧠 关键要点
- 记忆化 bug 本质上是 标识(identity) bug,而不是状态 bug。
- 任何会影响行为的属性都必须包含在记忆失效键中。
- 即使状态正确,陈旧闭包仍可能出现。
- 关闭记忆化只是变通方案,非根本解决办法。
- 性能优化需要额外的正确性约束。
🚀 最后思考
这个 bug:
- 看起来不可能出现。
- 在日志中“隐身”。
- 当你“简化”代码时就消失了。
从渲染快照的角度思考,问题便会迎刃而解。特别感谢 PrimeReact 维护者的快速反馈与合作,也感谢原始报告者提供的优秀复现案例。