React 19 与 Vue 4 新内核问题记录
记录 React 19 Compiler/RSC 与 Vue4 Signal/Vapor 落地时踩过的坑,每个问题都是实际遇到过并解决掉的。
问题一:开 React Compiler 后出现"缓存命中副作用消失"bug
现象:升级 React Compiler(Forget)后,某个组件的埋点偶发不触发。
原因:组件在渲染期写了 userCache.current.set(id, data)(useRef 的 Map)。Compiler 缓存的是表达式级结果——缓存命中时这段代码不执行,写副作用消失。这是代码 bug,不是编译器 bug。
解决:副作用移出渲染期(effect/事件回调);切换前先跑 eslint-plugin-react-hooks 的 compiler 诊断模式,按报错清单清理渲染期副作用和外部可变依赖。手写 useMemo/useCallback 残留场景:ref 可变对象参与渲染、每次语义上要新的对象。
问题二:'use client' 不是"这个文件在客户端跑"
现象:以为 'use client' 只标记当前文件,结果客户端 bundle 爆炸。
原因:它标记的是客户端边界入口,这个文件的所有 imports 会一起进客户端 bundle;且客户端组件不能 import 服务端组件,只能以 children 形式组合。
解决:把 'use client' 推到树的最深处(交互叶子),数据获取/重计算留服务端。props 必须可序列化(函数除 Server Actions 外传不过去)。
问题三:点赞失败后 UI 没回滚
现象:手写的"先 set 再请求"乐观更新,请求失败后点赞数停在错误值。
原因:自己维护乐观态,忘了写 undo;且没有 transition 语义,回滚时机不可靠。
解决:用 useOptimistic——它是指向 base state 的派生视图层,transition 结束自动丢弃乐观层(失败即回滚):
const [likes, addOptimistic] = useOptimistic(baseLikes, (cur, delta: number) => cur + delta);
function like() {
startTransition(async () => {
addOptimistic(1);
await likeAction(); // throw → transition 结束,乐观层自动消失
});
}
连续点 N 次 = N 层乐观叠加在同一 base 上;服务端以最终 base 为准。
问题四:Vue4 细粒度更新后为什么不需要 vdom diff
现象:想给团队讲清 Vue4 Signal 和 Vue3 的本质区别,发现背 Vue3 源码没用。
原因:Vue3 粒度是组件级——信号变了整个组件重渲染靠 diff;Vue4 Signal 的订阅者直接是"更新某个文本节点"的副作用函数,更新 = 执行几个闭包,不建 vnode 树。Vapor 模式在编译期把依赖声明进产物(setText(el0, () => count.value))。
解决:理解 Signal 三件套就能手写——订阅(get 收集)、触发(set 通知)、微任务批处理;computed 用 dirty 标记 + 值相等检查防止无效传播(min(A,B) 里改的是较大者时下游不重跑)。
问题五:搜索联想快速输入时闪旧值
现象:useDeferredValue 包了联想结果,快速输入时旧结果先渲染出来一瞬。
原因:deferred value 的语义就是"旧值立刻渲染,新值低优渲染"——闪旧值是特性不是 bug;且 transition/deferred 只管渲染优先级,不取消请求。
解决:请求侧只认最新一次(AbortController 取消旧的),渲染侧接受旧值或用占位骨架;输入框受控 state 保持高优(不包 transition),只有联想结果消费低优。选型口诀:useTransition 包"更新的发起",useDeferredValue 包"值的消费"(适合外部传 props 场景)。
