You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(target-react): stabilize member-mutated top-level <script> consts (useMemo)
A top-level `const X = new Set()` (or `[]`/`{}`) that is member-mutated as
cross-render scratch state but referenced only from a plain function — never
escaping into a useEffect dep — was emitted INLINE, so React re-created it
every render and silently reset the state. On the 5 setup-once targets the
`<script>` runs once, so the instance persists; React's re-running component
body broke the setup-once contract.
This is the root cause of the `code-mirror [react]` VR Matrix hang (red on
main since the CodeMirror G5 wave-2 batch b429107..df5f6aa): CodeMirrorDemo's
`const gutterSeen = new Set()` dedupe guard reset every render → the
`:data-cap="markGutterMount(line)"` render side-effect always re-incremented
`$data` → re-render → reconfigure → portal flushSync → infinite loop → 30s
timeout → the React demo never committed → the counted A/B editors never
appeared.
Fix: extend the React emitter with the const-analog of the reassigned-`let`→
`useRef` hoist. `collectMutatedInstanceBinders` finds top-level const/let
binders whose init is a fresh mutable instance (`new`/array/object literal) AND
which are member-mutated (`.add`/`.set`/`.push`/`.delete`/`X[k]=…`/`X.f++`/
`delete X[k]`); `tryWrapMutatedInstanceUseMemo` wraps each in
`useMemo(() => init, [])` so it is built once per instance. useMemo keeps the
binder name, so reads/mutations need no `.current` rewrite. Runs before the
escaping-const useMemo wrap so a binder that is both escaping and mutated gets
the stable `[]` identity (mutable state must never be re-created).
Conservative: a pure derived `const total = a + b` is never a fresh-instance
init and never member-mutated, so it is never frozen; reassigned `let`s are
excluded (that is the hoistModuleLet→useRef case). Member-mutated consts that
already escaped into a mount effect (e.g. MapLibre's marker/popup/feature Maps)
were already `useMemo(…, [])` via the escaping path — byte-identical output, no
drift.
Gates: minimal repro stabilizes seenConst while leaving pureDerived inline;
cold `--force` build 86/86 + typecheck 105/105 + test 64/64 (zero dist-parity /
snapshot drift — only React emit changes, only for the genuinely-non-escaping
case); Linux-Docker VR `code-mirror [react]` 3/3 green; full matrix 403 passed,
zero regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 commit comments