fix: review follow-ups for the recent reactive fixes (observer/react) - #257
Merged
Conversation
- batch: guard the Error.cause assignment so a frozen/non-extensible callback error no longer throws a TypeError from finally that would replace the in-flight exception (the #212 masking all over again). - batch: when the flush error cannot be attached (callback threw a non-Error, the Error already has a cause, or the object is frozen), console.warn instead of silently dropping the reaction's stack. - observe: reviving an unobserved reaction now resets everRan, so a failing first run after revival takes the firstRun auto-detach path instead of restoring an empty snapshot and becoming a live reaction with zero dependencies (interaction regression between #215 and #233). - runAsReaction: drop the per-rerun cleaners.slice() (releaseReaction never mutates the old array in place, so the reference is a valid snapshot) and the unreachable else-release branch. Co-Authored-By: Claude Code <noreply@anthropic.com>
view-wrapped class components silently lost reactivity after any componentWillUnmount that was not a real unmount — StrictMode's simulated remount and Suspense/Offscreen hide both null the _reactiveRender reaction, and nothing ever recreated it. Recreate in componentDidMount (the remount path replays cDM without re-rendering) and in render as a fallback; also treat an unobserved (first-run auto-detached) reaction as dead. useObserver gains the same unobserved-reaction guard as defense in depth — empirically React 19 remounts mount-suspended fibers (no hook state to preserve) and committed re-suspensions are healed by the subscribe cleanup/recreate path, but a non-null dead reaction must never be reused for tracking. Co-Authored-By: Claude Code <noreply@anthropic.com>
The committed-flag + queueMicrotask destroy only cancels StrictMode's same-flush re-setup. <Activity>/Offscreen hide→reveal spans separate commits, so the microtask destroys the container while the tree is merely hidden and the useRef-preserved ADM never recreates it — after reveal, children resolve against a null container. Rebuild the container in place during render (Activity reveal re-renders the tree) and re-arm the FinalizationRegistry backstop for the rebuilt container. Trade-off, noted in code: service state is lost while hidden. Also: - guard queueMicrotask with a Promise.resolve().then fallback for old RN JSC/Hermes environments (same targets the FinalizationRegistry and WeakRef fallbacks exist for), - drop the post-destroy registry re-register (provable no-op), - drop the dead `timmer` ADM field. Co-Authored-By: Claude Code <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
针对最近一批响应式修复(#228/#229/#233/#234 等)整体 review 后的跟进修复。Review 中每条候选问题都经过实证验证;本 PR 修复其中确认的 6 项正确性问题、1 项性能回归,并清理 3 项死代码,全部配回归测试(每个测试都验证过在未修复代码上失败)。
observer(db2ab84)
fnError.cause = flushError直接改调用方的错误对象;strict mode 下对象被Object.freeze时赋值自身抛 TypeError,从 finally 里替换在途异常——observer: batch() 的 flush 错误会吞掉回调自身的异常 #212 的错误掩蔽以另一种形式回归。改为 try/catch 守卫,原始异常优先。everRan,复活后首跑失败按 firstRun 语义自动脱管,而不是带着空快照走 restore 变成「存活但零依赖」的僵尸。cleaners.slice()(releaseReaction不原地修改旧数组,引用即合法快照);删除不可达的else releaseReaction分支。react
死 reaction 重建(00da83e)
审计修正:React 19 下首渲染挂起的组件会被整体重挂载(fiber/hook 状态不保留),函数组件由 subscribe cleanup/recreate 兜底自愈;真正可复现的是类组件路径,且更严重——
view类组件在普通 StrictMode 下就永久失去响应式:模拟卸载调用componentWillUnmount置空_reactiveRender,重挂载只重放componentDidMount、不触发 render,无任何重建。_createReactiveRender(),在componentDidMount(重建 +forceUpdate重收集依赖)与render(兜底)检测 reaction 死亡(null 或 unobserved)并重建useObserver增加 unobserved 检查作纵深防御bindServices 隐藏态容器(48879ee)
审计修正:React 19 中 Suspense 重新隐藏不拆除
useEffect(只拆 layoutEffect),<Activity>隐藏才会。committed+ microtask 的销毁只能取消 StrictMode 同帧重挂载;Activity/Offscreen 的 hide→reveal 跨 commit,microtask 在中间销毁容器,而 useRef 保留的 ADM 让 createADM 不再执行,reveal 后子组件解析到 null 容器。queueMicrotask运行时探测 + Promise 降级(旧 RN JSC/Hermes,与包内 FinalizationRegistry/WeakRef 降级同一批环境)timmer测试
tsc --noEmit无错误🤖 Generated with Claude Code