From 7b233b8221dc955323a08389a0639eab9911ddd2 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 14:55:17 -0700 Subject: [PATCH 1/7] [Compiler] Add test fixtures reproducing nullable closure cache key bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 10 test fixtures that reproduce the bug where the compiler hoists property accesses from closures into cache key checks that crash when the base object is nullable. These fixtures currently show the BUGGY behavior — the compiled output contains non-optional cache key comparisons like `$[0] !== user.name` that throw TypeError when `user` is null, even though the source code guards against this with early returns or only accesses the property inside event handlers. Covered patterns: - Nullable prop + onClick handler + early return guard - TypeScript non-null assertion (!) inside closure - Mixed optional chain (user?.company.name) inside closure - Deep property access on nullable base - Closure passed as JSX prop with nullable state - Function passed to custom hook - TypeScript assertion function pattern - Multiple closures on different nullable props - Optional chain in render + unconditional in closure - Returned function accessing nullable prop Related: facebook/react#34752, facebook/react#34194, facebook/react#35762 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...nullable-closure-assert-function.expect.md | 90 +++++++++++++ ...isted-nullable-closure-assert-function.tsx | 32 +++++ ...losure-conditional-render-access.expect.md | 90 +++++++++++++ ...able-closure-conditional-render-access.tsx | 28 ++++ ...d-nullable-closure-hook-argument.expect.md | 82 ++++++++++++ ...hoisted-nullable-closure-hook-argument.tsx | 23 ++++ ...ble-closure-mixed-optional-chain.expect.md | 75 +++++++++++ ...-nullable-closure-mixed-optional-chain.tsx | 24 ++++ ...llable-closure-multiple-closures.expect.md | 124 ++++++++++++++++++ ...ted-nullable-closure-multiple-closures.tsx | 38 ++++++ ...nullable-closure-nested-property.expect.md | 97 ++++++++++++++ ...isted-nullable-closure-nested-property.tsx | 31 +++++ ...nullable-closure-property-access.expect.md | 89 +++++++++++++ ...isted-nullable-closure-property-access.tsx | 25 ++++ ...llable-closure-returned-function.expect.md | 71 ++++++++++ ...ted-nullable-closure-returned-function.tsx | 21 +++ ...le-closure-ts-non-null-assertion.expect.md | 83 ++++++++++++ ...nullable-closure-ts-non-null-assertion.tsx | 22 ++++ ...ullable-closure-use-effect-event.expect.md | 81 ++++++++++++ ...sted-nullable-closure-use-effect-event.tsx | 20 +++ 20 files changed, 1146 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md new file mode 100644 index 000000000000..9846e313482d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md @@ -0,0 +1,90 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: TypeScript assertion function pattern. `planPeriod.id` after the + * assertion is used as a cache key, crashing when planPeriod is null. + * The compiler doesn't understand that the assertion narrows the type. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function assertIsNotEmpty( + value: TValue | null | undefined +): asserts value is TValue { + if (value == null) throw new Error('assertion failure'); +} + +function Component({ + planPeriod, +}: { + planPeriod: {id: string} | null; +}) { + const callback = () => { + assertIsNotEmpty(planPeriod?.id); + console.log(planPeriod.id); + }; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{planPeriod: {id: 'p1'}}], + sequentialRenders: [{planPeriod: {id: 'p1'}}, {planPeriod: {id: 'p2'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: TypeScript assertion function pattern. `planPeriod.id` after the + * assertion is used as a cache key, crashing when planPeriod is null. + * The compiler doesn't understand that the assertion narrows the type. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function assertIsNotEmpty(value) { + if (value == null) { + throw new Error("assertion failure"); + } +} + +function Component(t0) { + const $ = _c(2); + const { planPeriod } = t0; + let t1; + if ($[0] !== planPeriod.id) { + const callback = () => { + assertIsNotEmpty(planPeriod?.id); + console.log(planPeriod.id); + }; + t1 = ; + $[0] = planPeriod.id; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ planPeriod: { id: "p1" } }], + sequentialRenders: [ + { planPeriod: { id: "p1" } }, + { planPeriod: { id: "p2" } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]"}
+
{"onClick":"[[ function params=0 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.tsx new file mode 100644 index 000000000000..821546e59657 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.tsx @@ -0,0 +1,32 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: TypeScript assertion function pattern. `planPeriod.id` after the + * assertion is used as a cache key, crashing when planPeriod is null. + * The compiler doesn't understand that the assertion narrows the type. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function assertIsNotEmpty( + value: TValue | null | undefined +): asserts value is TValue { + if (value == null) throw new Error('assertion failure'); +} + +function Component({ + planPeriod, +}: { + planPeriod: {id: string} | null; +}) { + const callback = () => { + assertIsNotEmpty(planPeriod?.id); + console.log(planPeriod.id); + }; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{planPeriod: {id: 'p1'}}], + sequentialRenders: [{planPeriod: {id: 'p1'}}, {planPeriod: {id: 'p2'}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md new file mode 100644 index 000000000000..a21e5b14dade --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md @@ -0,0 +1,90 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Optional chain `user?.name` in render + unconditional `user.email` + * in closure. The closure's `user.email` makes the compiler think `user` is + * non-null, converting the render's `user?.name` to `user.name` in cache keys. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component({ + user, +}: { + user: {name: string; email: string} | null; +}) { + const sendEmail = () => { + console.log(user.email); + }; + return {user?.name}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice', email: 'alice@example.com'}}], + sequentialRenders: [ + {user: {name: 'Alice', email: 'alice@example.com'}}, + {user: {name: 'Bob', email: 'bob@example.com'}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: Optional chain `user?.name` in render + unconditional `user.email` + * in closure. The closure's `user.email` makes the compiler think `user` is + * non-null, converting the render's `user?.name` to `user.name` in cache keys. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component(t0) { + const $ = _c(5); + const { user } = t0; + let t1; + if ($[0] !== user.email) { + t1 = () => { + console.log(user.email); + }; + $[0] = user.email; + $[1] = t1; + } else { + t1 = $[1]; + } + const sendEmail = t1; + + const t2 = user?.name; + let t3; + if ($[2] !== sendEmail || $[3] !== t2) { + t3 = {t2}; + $[2] = sendEmail; + $[3] = t2; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ user: { name: "Alice", email: "alice@example.com" } }], + sequentialRenders: [ + { user: { name: "Alice", email: "alice@example.com" } }, + { user: { name: "Bob", email: "bob@example.com" } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]","children":"Alice"}
+
{"onClick":"[[ function params=0 ]]","children":"Bob"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.tsx new file mode 100644 index 000000000000..80fd5f04ea87 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.tsx @@ -0,0 +1,28 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Optional chain `user?.name` in render + unconditional `user.email` + * in closure. The closure's `user.email` makes the compiler think `user` is + * non-null, converting the render's `user?.name` to `user.name` in cache keys. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component({ + user, +}: { + user: {name: string; email: string} | null; +}) { + const sendEmail = () => { + console.log(user.email); + }; + return {user?.name}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice', email: 'alice@example.com'}}], + sequentialRenders: [ + {user: {name: 'Alice', email: 'alice@example.com'}}, + {user: {name: 'Bob', email: 'bob@example.com'}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md new file mode 100644 index 000000000000..4e921cb0dbd3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md @@ -0,0 +1,82 @@ + +## Input + +```javascript +import {Stringify, useIdentity} from 'shared-runtime'; + +/** + * Bug: Function passed as argument to a custom hook. The compiler assumes + * hook arguments are invoked and hoists `item.id` from the closure into + * a cache key that crashes when item is null. + * + * Related: https://github.com/facebook/react/issues/34194 + */ +function Component({item}: {item: {id: string} | null}) { + const processItem = () => { + return item.id; + }; + useIdentity(processItem); + if (!item) return null; + return {item.id}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{item: {id: 'abc'}}], + sequentialRenders: [{item: {id: 'abc'}}, {item: {id: 'def'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, useIdentity } from "shared-runtime"; + +/** + * Bug: Function passed as argument to a custom hook. The compiler assumes + * hook arguments are invoked and hoists `item.id` from the closure into + * a cache key that crashes when item is null. + * + * Related: https://github.com/facebook/react/issues/34194 + */ +function Component(t0) { + const $ = _c(4); + const { item } = t0; + let t1; + if ($[0] !== item.id) { + t1 = () => item.id; + $[0] = item.id; + $[1] = t1; + } else { + t1 = $[1]; + } + const processItem = t1; + + useIdentity(processItem); + if (!item) { + return null; + } + let t2; + if ($[2] !== item.id) { + t2 = {item.id}; + $[2] = item.id; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ item: { id: "abc" } }], + sequentialRenders: [{ item: { id: "abc" } }, { item: { id: "def" } }], +}; + +``` + +### Eval output +(kind: ok)
{"children":"abc"}
+
{"children":"def"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.tsx new file mode 100644 index 000000000000..3b9193b85d56 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.tsx @@ -0,0 +1,23 @@ +import {Stringify, useIdentity} from 'shared-runtime'; + +/** + * Bug: Function passed as argument to a custom hook. The compiler assumes + * hook arguments are invoked and hoists `item.id` from the closure into + * a cache key that crashes when item is null. + * + * Related: https://github.com/facebook/react/issues/34194 + */ +function Component({item}: {item: {id: string} | null}) { + const processItem = () => { + return item.id; + }; + useIdentity(processItem); + if (!item) return null; + return {item.id}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{item: {id: 'abc'}}], + sequentialRenders: [{item: {id: 'abc'}}, {item: {id: 'def'}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md new file mode 100644 index 000000000000..68f356b2c6ac --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md @@ -0,0 +1,75 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: `user?.company.name` inside a closure — the compiler strips the `?.` + * when computing cache keys, producing `user.company.name` which crashes + * when user is null. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component({user}: {user: {company: {name: string}} | null}) { + const handleClick = () => { + console.log(user?.company.name); + }; + return Click; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {company: {name: 'Acme'}}}], + sequentialRenders: [ + {user: {company: {name: 'Acme'}}}, + {user: {company: {name: 'Corp'}}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: `user?.company.name` inside a closure — the compiler strips the `?.` + * when computing cache keys, producing `user.company.name` which crashes + * when user is null. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component(t0) { + const $ = _c(2); + const { user } = t0; + let t1; + if ($[0] !== user?.company.name) { + const handleClick = () => { + console.log(user?.company.name); + }; + t1 = Click; + $[0] = user?.company.name; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ user: { company: { name: "Acme" } } }], + sequentialRenders: [ + { user: { company: { name: "Acme" } } }, + { user: { company: { name: "Corp" } } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]","children":"Click"}
+
{"onClick":"[[ function params=0 ]]","children":"Click"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.tsx new file mode 100644 index 000000000000..1709614bded4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.tsx @@ -0,0 +1,24 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: `user?.company.name` inside a closure — the compiler strips the `?.` + * when computing cache keys, producing `user.company.name` which crashes + * when user is null. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component({user}: {user: {company: {name: string}} | null}) { + const handleClick = () => { + console.log(user?.company.name); + }; + return Click; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {company: {name: 'Acme'}}}], + sequentialRenders: [ + {user: {company: {name: 'Acme'}}}, + {user: {company: {name: 'Corp'}}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md new file mode 100644 index 000000000000..bcadac93b56b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md @@ -0,0 +1,124 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Multiple closures accessing different nullable props. Both `user.name` + * and `post.title` are hoisted as cache keys that crash when either is null. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component({ + user, + post, +}: { + user: {name: string} | null; + post: {title: string} | null; +}) { + const handleUser = () => { + console.log(user.name); + }; + const handlePost = () => { + console.log(post.title); + }; + if (!user || !post) return null; + return ( + + {user.name} + {post.title} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice'}, post: {title: 'Hello'}}], + sequentialRenders: [ + {user: {name: 'Alice'}, post: {title: 'Hello'}}, + {user: {name: 'Bob'}, post: {title: 'World'}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: Multiple closures accessing different nullable props. Both `user.name` + * and `post.title` are hoisted as cache keys that crash when either is null. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component(t0) { + const $ = _c(9); + const { user, post } = t0; + let t1; + if ($[0] !== user.name) { + t1 = () => { + console.log(user.name); + }; + $[0] = user.name; + $[1] = t1; + } else { + t1 = $[1]; + } + const handleUser = t1; + let t2; + if ($[2] !== post.title) { + t2 = () => { + console.log(post.title); + }; + $[2] = post.title; + $[3] = t2; + } else { + t2 = $[3]; + } + const handlePost = t2; + + if (!user || !post) { + return null; + } + let t3; + if ( + $[4] !== handlePost || + $[5] !== handleUser || + $[6] !== post.title || + $[7] !== user.name + ) { + t3 = ( + + {user.name} + {post.title} + + ); + $[4] = handlePost; + $[5] = handleUser; + $[6] = post.title; + $[7] = user.name; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ user: { name: "Alice" }, post: { title: "Hello" } }], + sequentialRenders: [ + { user: { name: "Alice" }, post: { title: "Hello" } }, + { user: { name: "Bob" }, post: { title: "World" } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onUser":"[[ function params=0 ]]","onPost":"[[ function params=0 ]]","children":["Alice","Hello"]}
+
{"onUser":"[[ function params=0 ]]","onPost":"[[ function params=0 ]]","children":["Bob","World"]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.tsx new file mode 100644 index 000000000000..18977caf664a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.tsx @@ -0,0 +1,38 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Multiple closures accessing different nullable props. Both `user.name` + * and `post.title` are hoisted as cache keys that crash when either is null. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component({ + user, + post, +}: { + user: {name: string} | null; + post: {title: string} | null; +}) { + const handleUser = () => { + console.log(user.name); + }; + const handlePost = () => { + console.log(post.title); + }; + if (!user || !post) return null; + return ( + + {user.name} + {post.title} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice'}, post: {title: 'Hello'}}], + sequentialRenders: [ + {user: {name: 'Alice'}, post: {title: 'Hello'}}, + {user: {name: 'Bob'}, post: {title: 'World'}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md new file mode 100644 index 000000000000..2d4cdcff7523 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md @@ -0,0 +1,97 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Deep property access on nullable base inside closure. The compiler + * hoists `post.author.profile.avatar` as a cache key that crashes when + * post is null, even though the early return guard prevents rendering. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component({ + post, +}: { + post: {author: {profile: {avatar: string}}} | null; +}) { + const handleClick = () => { + console.log(post.author.profile.avatar); + }; + if (!post) return null; + return ( + {post.author.profile.avatar} + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{post: {author: {profile: {avatar: 'pic.jpg'}}}}], + sequentialRenders: [ + {post: {author: {profile: {avatar: 'pic.jpg'}}}}, + {post: {author: {profile: {avatar: 'new.jpg'}}}}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: Deep property access on nullable base inside closure. The compiler + * hoists `post.author.profile.avatar` as a cache key that crashes when + * post is null, even though the early return guard prevents rendering. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component(t0) { + const $ = _c(5); + const { post } = t0; + let t1; + if ($[0] !== post.author.profile.avatar) { + t1 = () => { + console.log(post.author.profile.avatar); + }; + $[0] = post.author.profile.avatar; + $[1] = t1; + } else { + t1 = $[1]; + } + const handleClick = t1; + + if (!post) { + return null; + } + let t2; + if ($[2] !== handleClick || $[3] !== post.author.profile.avatar) { + t2 = ( + {post.author.profile.avatar} + ); + $[2] = handleClick; + $[3] = post.author.profile.avatar; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ post: { author: { profile: { avatar: "pic.jpg" } } } }], + sequentialRenders: [ + { post: { author: { profile: { avatar: "pic.jpg" } } } }, + { post: { author: { profile: { avatar: "new.jpg" } } } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]","children":"pic.jpg"}
+
{"onClick":"[[ function params=0 ]]","children":"new.jpg"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.tsx new file mode 100644 index 000000000000..14b7945d78da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.tsx @@ -0,0 +1,31 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Deep property access on nullable base inside closure. The compiler + * hoists `post.author.profile.avatar` as a cache key that crashes when + * post is null, even though the early return guard prevents rendering. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component({ + post, +}: { + post: {author: {profile: {avatar: string}}} | null; +}) { + const handleClick = () => { + console.log(post.author.profile.avatar); + }; + if (!post) return null; + return ( + {post.author.profile.avatar} + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{post: {author: {profile: {avatar: 'pic.jpg'}}}}], + sequentialRenders: [ + {post: {author: {profile: {avatar: 'pic.jpg'}}}}, + {post: {author: {profile: {avatar: 'new.jpg'}}}}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md new file mode 100644 index 000000000000..2167367abf55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md @@ -0,0 +1,89 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: The compiler hoists `user.name` from the onClick closure into a cache + * key check that runs during render. When `user` is null, this crashes with + * TypeError even though the source code guards with an early return. + * + * The compiled output should use `user?.name` (optional) in the cache key, + * not `user.name` (non-optional). + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component({user}: {user: {name: string} | null}) { + const handleClick = () => { + console.log(user.name); + }; + if (!user) return null; + return {user.name}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice'}}], + sequentialRenders: [{user: {name: 'Alice'}}, {user: {name: 'Bob'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: The compiler hoists `user.name` from the onClick closure into a cache + * key check that runs during render. When `user` is null, this crashes with + * TypeError even though the source code guards with an early return. + * + * The compiled output should use `user?.name` (optional) in the cache key, + * not `user.name` (non-optional). + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component(t0) { + const $ = _c(5); + const { user } = t0; + let t1; + if ($[0] !== user.name) { + t1 = () => { + console.log(user.name); + }; + $[0] = user.name; + $[1] = t1; + } else { + t1 = $[1]; + } + const handleClick = t1; + + if (!user) { + return null; + } + let t2; + if ($[2] !== handleClick || $[3] !== user.name) { + t2 = {user.name}; + $[2] = handleClick; + $[3] = user.name; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ user: { name: "Alice" } }], + sequentialRenders: [{ user: { name: "Alice" } }, { user: { name: "Bob" } }], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]","children":"Alice"}
+
{"onClick":"[[ function params=0 ]]","children":"Bob"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.tsx new file mode 100644 index 000000000000..c46f20929818 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.tsx @@ -0,0 +1,25 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: The compiler hoists `user.name` from the onClick closure into a cache + * key check that runs during render. When `user` is null, this crashes with + * TypeError even though the source code guards with an early return. + * + * The compiled output should use `user?.name` (optional) in the cache key, + * not `user.name` (non-optional). + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function Component({user}: {user: {name: string} | null}) { + const handleClick = () => { + console.log(user.name); + }; + if (!user) return null; + return {user.name}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice'}}], + sequentialRenders: [{user: {name: 'Alice'}}, {user: {name: 'Bob'}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md new file mode 100644 index 000000000000..e332c5337bfa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +import {createHookWrapper} from 'shared-runtime'; + +/** + * Bug: Returned function accessing nullable prop. The compiler classifies + * returned functions as "assumed invoked" and hoists `item.id` as a cache + * key that crashes when item is null. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function useHandler({item}: {item: {id: string} | null}) { + const handler = () => { + console.log(item.id); + }; + return handler; +} + +export const FIXTURE_ENTRYPOINT = { + fn: createHookWrapper(useHandler), + params: [{item: {id: 'abc'}}], + sequentialRenders: [{item: {id: 'abc'}}, {item: {id: 'def'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { createHookWrapper } from "shared-runtime"; + +/** + * Bug: Returned function accessing nullable prop. The compiler classifies + * returned functions as "assumed invoked" and hoists `item.id` as a cache + * key that crashes when item is null. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function useHandler(t0) { + const $ = _c(2); + const { item } = t0; + let t1; + if ($[0] !== item.id) { + t1 = () => { + console.log(item.id); + }; + $[0] = item.id; + $[1] = t1; + } else { + t1 = $[1]; + } + const handler = t1; + + return handler; +} + +export const FIXTURE_ENTRYPOINT = { + fn: createHookWrapper(useHandler), + params: [{ item: { id: "abc" } }], + sequentialRenders: [{ item: { id: "abc" } }, { item: { id: "def" } }], +}; + +``` + +### Eval output +(kind: ok)
{"result":{"kind":"Function"},"shouldInvokeFns":true}
+
{"result":{"kind":"Function"},"shouldInvokeFns":true}
+logs: ['abc','def'] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.tsx new file mode 100644 index 000000000000..3fbac77bd5d7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.tsx @@ -0,0 +1,21 @@ +import {createHookWrapper} from 'shared-runtime'; + +/** + * Bug: Returned function accessing nullable prop. The compiler classifies + * returned functions as "assumed invoked" and hoists `item.id` as a cache + * key that crashes when item is null. + * + * Related: https://github.com/facebook/react/issues/35762 + */ +function useHandler({item}: {item: {id: string} | null}) { + const handler = () => { + console.log(item.id); + }; + return handler; +} + +export const FIXTURE_ENTRYPOINT = { + fn: createHookWrapper(useHandler), + params: [{item: {id: 'abc'}}], + sequentialRenders: [{item: {id: 'abc'}}, {item: {id: 'def'}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md new file mode 100644 index 000000000000..d159106cc2a5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: TypeScript non-null assertion (!) is transparent to the compiler. + * `data!.id` inside the closure is lowered as `data.id`, causing the compiler + * to hoist `data.id` as a cache key that crashes when data is undefined. + * + * Related: https://github.com/facebook/react/issues/34194 + */ +function Component({data}: {data: {id: string} | undefined}) { + const handleClick = () => { + console.log(data!.id); + }; + if (!data) return null; + return {data.id}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{data: {id: 'abc'}}], + sequentialRenders: [{data: {id: 'abc'}}, {data: {id: 'def'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: TypeScript non-null assertion (!) is transparent to the compiler. + * `data!.id` inside the closure is lowered as `data.id`, causing the compiler + * to hoist `data.id` as a cache key that crashes when data is undefined. + * + * Related: https://github.com/facebook/react/issues/34194 + */ +function Component(t0) { + const $ = _c(5); + const { data } = t0; + let t1; + if ($[0] !== data.id) { + t1 = () => { + console.log(data.id); + }; + $[0] = data.id; + $[1] = t1; + } else { + t1 = $[1]; + } + const handleClick = t1; + + if (!data) { + return null; + } + let t2; + if ($[2] !== data.id || $[3] !== handleClick) { + t2 = {data.id}; + $[2] = data.id; + $[3] = handleClick; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ data: { id: "abc" } }], + sequentialRenders: [{ data: { id: "abc" } }, { data: { id: "def" } }], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]","children":"abc"}
+
{"onClick":"[[ function params=0 ]]","children":"def"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.tsx new file mode 100644 index 000000000000..4faed5411159 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.tsx @@ -0,0 +1,22 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: TypeScript non-null assertion (!) is transparent to the compiler. + * `data!.id` inside the closure is lowered as `data.id`, causing the compiler + * to hoist `data.id` as a cache key that crashes when data is undefined. + * + * Related: https://github.com/facebook/react/issues/34194 + */ +function Component({data}: {data: {id: string} | undefined}) { + const handleClick = () => { + console.log(data!.id); + }; + if (!data) return null; + return {data.id}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{data: {id: 'abc'}}], + sequentialRenders: [{data: {id: 'abc'}}, {data: {id: 'def'}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md new file mode 100644 index 000000000000..3f6f2bcfdcd4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Closure passed as JSX prop accesses nullable state. The compiler hoists + * `data.value` into a cache key that crashes because data could be null. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component({data}: {data: {value: string} | null}) { + const onData = () => { + console.log(data.value); + }; + return {data?.value}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{data: {value: 'hello'}}], + sequentialRenders: [{data: {value: 'hello'}}, {data: {value: 'world'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Bug: Closure passed as JSX prop accesses nullable state. The compiler hoists + * `data.value` into a cache key that crashes because data could be null. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component(t0) { + const $ = _c(5); + const { data } = t0; + let t1; + if ($[0] !== data.value) { + t1 = () => { + console.log(data.value); + }; + $[0] = data.value; + $[1] = t1; + } else { + t1 = $[1]; + } + const onData = t1; + + const t2 = data?.value; + let t3; + if ($[2] !== onData || $[3] !== t2) { + t3 = {t2}; + $[2] = onData; + $[3] = t2; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ data: { value: "hello" } }], + sequentialRenders: [ + { data: { value: "hello" } }, + { data: { value: "world" } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onData":"[[ function params=0 ]]","children":"hello"}
+
{"onData":"[[ function params=0 ]]","children":"world"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.tsx new file mode 100644 index 000000000000..27a03a9cdbe2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.tsx @@ -0,0 +1,20 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Bug: Closure passed as JSX prop accesses nullable state. The compiler hoists + * `data.value` into a cache key that crashes because data could be null. + * + * Related: https://github.com/facebook/react/issues/34752 + */ +function Component({data}: {data: {value: string} | null}) { + const onData = () => { + console.log(data.value); + }; + return {data?.value}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{data: {value: 'hello'}}], + sequentialRenders: [{data: {value: 'hello'}}, {data: {value: 'world'}}], +}; From 10109fbea3fe4a72b2746f242250cc78ae03e2d5 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 14:55:40 -0700 Subject: [PATCH 2/7] [Compiler] Add correctness guard fixtures for sync invocation paths Add 2 fixtures that verify correct behavior that must NOT regress: 1. nullable-closure-with-render-access-is-safe: When a property is accessed directly during render (not inside a closure), it correctly proves non-nullness. Cache key `user.name` is correct here. 2. nullable-closure-direct-call-is-safe: When a closure is called synchronously during render via a direct call, its property accesses execute during render and can safely prove non-nullness. Cache key `obj.value` is correct here. These fixtures ensure the upcoming fix preserves optimization for sync call paths while only restricting deferred (JSX/hook/return) paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...able-closure-direct-call-is-safe.expect.md | 73 +++++++++++++++++ .../nullable-closure-direct-call-is-safe.tsx | 19 +++++ ...osure-with-render-access-is-safe.expect.md | 78 +++++++++++++++++++ ...ble-closure-with-render-access-is-safe.tsx | 21 +++++ 4 files changed, 191 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.expect.md new file mode 100644 index 000000000000..41b55c05619e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.expect.md @@ -0,0 +1,73 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Correctness guard: When a closure is directly called during render, + * it executes synchronously and its property accesses prove non-nullness. + * The cache key should remain `obj.value` (non-optional). This fixture + * must NOT change after the nullable-closure fix. + */ +function Component({obj}: {obj: {value: number}}) { + const getValue = () => obj.value; + const value = getValue(); + return {value}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{obj: {value: 1}}], + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Correctness guard: When a closure is directly called during render, + * it executes synchronously and its property accesses prove non-nullness. + * The cache key should remain `obj.value` (non-optional). This fixture + * must NOT change after the nullable-closure fix. + */ +function Component(t0) { + const $ = _c(4); + const { obj } = t0; + let t1; + if ($[0] !== obj.value) { + const getValue = () => obj.value; + t1 = getValue(); + $[0] = obj.value; + $[1] = t1; + } else { + t1 = $[1]; + } + const value = t1; + let t2; + if ($[2] !== value) { + t2 = {value}; + $[2] = value; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ obj: { value: 1 } }], + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }], +}; + +``` + +### Eval output +(kind: ok)
{"children":1}
+
{"children":2}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.tsx new file mode 100644 index 000000000000..507fd7679df4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-direct-call-is-safe.tsx @@ -0,0 +1,19 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Correctness guard: When a closure is directly called during render, + * it executes synchronously and its property accesses prove non-nullness. + * The cache key should remain `obj.value` (non-optional). This fixture + * must NOT change after the nullable-closure fix. + */ +function Component({obj}: {obj: {value: number}}) { + const getValue = () => obj.value; + const value = getValue(); + return {value}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{obj: {value: 1}}], + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.expect.md new file mode 100644 index 000000000000..eeececdb2d3b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.expect.md @@ -0,0 +1,78 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +/** + * Correctness guard: When there is a render-time property access (user.name) + * in the outer function body, it proves non-nullness at that point. The cache + * key should remain `user.name` (non-optional). This fixture must NOT change + * after the nullable-closure fix. + */ +function Component({user}: {user: {name: string}}) { + const name = user.name; + const handleClick = () => { + console.log(user.name); + }; + return {name}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice'}}], + sequentialRenders: [{user: {name: 'Alice'}}, {user: {name: 'Bob'}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +/** + * Correctness guard: When there is a render-time property access (user.name) + * in the outer function body, it proves non-nullness at that point. The cache + * key should remain `user.name` (non-optional). This fixture must NOT change + * after the nullable-closure fix. + */ +function Component(t0) { + const $ = _c(5); + const { user } = t0; + const name = user.name; + let t1; + if ($[0] !== user.name) { + t1 = () => { + console.log(user.name); + }; + $[0] = user.name; + $[1] = t1; + } else { + t1 = $[1]; + } + const handleClick = t1; + let t2; + if ($[2] !== handleClick || $[3] !== name) { + t2 = {name}; + $[2] = handleClick; + $[3] = name; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ user: { name: "Alice" } }], + sequentialRenders: [{ user: { name: "Alice" } }, { user: { name: "Bob" } }], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":"[[ function params=0 ]]","children":"Alice"}
+
{"onClick":"[[ function params=0 ]]","children":"Bob"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.tsx new file mode 100644 index 000000000000..b0b052e9a467 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nullable-closure-with-render-access-is-safe.tsx @@ -0,0 +1,21 @@ +import {Stringify} from 'shared-runtime'; + +/** + * Correctness guard: When there is a render-time property access (user.name) + * in the outer function body, it proves non-nullness at that point. The cache + * key should remain `user.name` (non-optional). This fixture must NOT change + * after the nullable-closure fix. + */ +function Component({user}: {user: {name: string}}) { + const name = user.name; + const handleClick = () => { + console.log(user.name); + }; + return {name}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {name: 'Alice'}}], + sequentialRenders: [{user: {name: 'Alice'}}, {user: {name: 'Bob'}}], +}; From eaf1f422ff960c12ec1f8dc4485245a6e03f8681 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 14:56:18 -0700 Subject: [PATCH 3/7] [Compiler] Split assumedInvokedFns into sync vs deferred categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root cause of the nullable closure cache key bug is that getAssumedInvokedFunctions treated all inner functions identically — direct calls, JSX event handlers, hook callbacks, and returned functions were all put into a single set. Property accesses from ALL of them were propagated as non-null assumptions to the outer scope. This is incorrect for deferred functions: JSX event handlers don't run during render, hook callbacks (useEffect, useCallback, etc.) run after render, and returned functions run in the caller's context. Their property accesses cannot prove non-nullness at render time. Split getAssumedInvokedFunctions into two categories: - syncInvoked: Functions called synchronously during render (direct function calls like `cb()`). Their property accesses safely prove non-nullness at the call site. - deferredInvoked: Functions invoked later (JSX event handlers, hook callbacks, returned functions). Their property accesses should NOT prove non-nullness during render. In collectNonNullsInBlocks, only propagate assumedNonNullObjects from syncInvoked functions to the outer scope. Deferred functions still get their own internal hoistable analysis but don't leak non-null outward. The transitive closure propagates categories correctly: if a sync function calls another function, that function is also sync. If a deferred function calls another, it's also deferred. If a function appears in both sets, sync wins. Note: useMemo/useCallback are already lowered to StartMemoize/ FinishMemoize by dropManualMemoization (always enabled) before this pass runs, so they never appear as hook CallExpressions here. Fixes https://github.com/facebook/react/issues/34752 Fixes https://github.com/facebook/react/issues/34194 Fixes https://github.com/facebook/react/issues/35762 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/HIR/CollectHoistablePropertyLoads.ts | 106 ++++++++++++++---- 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts index c47a41145157..19bac9b39d73 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -167,9 +167,21 @@ type CollectHoistablePropertyLoadsContext = { nestedFnImmutableContext: ReadonlySet | null; /** * Functions which are assumed to be eventually called (as opposed to ones which might - * not be called, e.g. the 0th argument of Array.map) + * not be called, e.g. the 0th argument of Array.map). + * + * Split into two categories: + * - syncInvoked: Functions called synchronously during render (direct calls). + * Property accesses from these can safely prove non-nullness at the outer scope. + * - deferredInvoked: Functions invoked later (JSX event handlers, hook callbacks, + * returned functions). Property accesses from these should NOT prove non-nullness + * at the outer scope because they don't execute during render. */ - assumedInvokedFns: ReadonlySet; + assumedInvokedFns: AssumedInvokedFunctions; +}; + +type AssumedInvokedFunctions = { + syncInvoked: ReadonlySet; + deferredInvoked: ReadonlySet; }; function collectHoistablePropertyLoadsImpl( fn: HIRFunction, @@ -429,7 +441,11 @@ function collectNonNullsInBlocks( } if (instr.value.kind === 'FunctionExpression') { const innerFn = instr.value.loweredFunc; - if (context.assumedInvokedFns.has(innerFn)) { + const isSyncInvoked = + context.assumedInvokedFns.syncInvoked.has(innerFn); + const isDeferredInvoked = + context.assumedInvokedFns.deferredInvoked.has(innerFn); + if (isSyncInvoked || isDeferredInvoked) { const innerHoistableMap = collectHoistablePropertyLoadsImpl( innerFn.func, { @@ -445,11 +461,23 @@ function collectNonNullsInBlocks( ), }, ); - const innerHoistables = assertNonNull( - innerHoistableMap.get(innerFn.func.body.entry), - ); - for (const entry of innerHoistables.assumedNonNullObjects) { - assumedNonNullObjects.add(entry); + /** + * Only propagate assumedNonNullObjects from synchronously-invoked + * inner functions (direct calls during render). Deferred functions + * (JSX event handlers, hook callbacks, returned functions) don't + * execute during render, so their property accesses cannot prove + * non-nullness at render time. + * + * See: https://github.com/facebook/react/issues/34752 + * https://github.com/facebook/react/issues/35762 + */ + if (isSyncInvoked) { + const innerHoistables = assertNonNull( + innerHoistableMap.get(innerFn.func.body.entry), + ); + for (const entry of innerHoistables.assumedNonNullObjects) { + assumedNonNullObjects.add(entry); + } } } } else if ( @@ -700,8 +728,9 @@ function getAssumedInvokedFunctions( IdentifierId, {fn: LoweredFunction; mayInvoke: Set} > = new Map(), -): ReadonlySet { - const hoistableFunctions = new Set(); +): AssumedInvokedFunctions { + const syncInvoked = new Set(); + const deferredInvoked = new Set(); /** * Step 1: Conservatively collect identifier to function expression mappings */ @@ -734,6 +763,13 @@ function getAssumedInvokedFunctions( * Step 2: Forward pass to do analysis of assumed function calls. Note that * this is conservative and does not count indirect references through * containers (e.g. `return {cb: () => {...}})`). + * + * Functions are classified into two categories: + * - syncInvoked: Called synchronously during render (direct function calls). + * Their property accesses can safely prove non-nullness at the call site. + * - deferredInvoked: Called later, not during render (JSX event handlers, + * hook callbacks, returned functions). Their property accesses should NOT + * be used to prove non-nullness during render. */ for (const block of fn.body.blocks.values()) { for (const {lvalue, value} of block.instructions) { @@ -742,24 +778,29 @@ function getAssumedInvokedFunctions( const maybeHook = getHookKind(fn.env, callee.identifier); const maybeLoweredFunc = temporaries.get(callee.identifier.id); if (maybeLoweredFunc != null) { - // Direct calls - hoistableFunctions.add(maybeLoweredFunc.fn); + // Direct calls execute synchronously during render + syncInvoked.add(maybeLoweredFunc.fn); } else if (maybeHook != null) { /** - * Assume arguments to all hooks are safe to invoke + * Hook arguments are classified as deferred — they may or may not + * be invoked during render. Note: useMemo/useCallback are already + * lowered to StartMemoize/FinishMemoize before this pass runs + * (dropManualMemoization is always enabled), so they won't appear + * as hook CallExpressions here. */ for (const arg of value.args) { if (arg.kind === 'Identifier') { const maybeLoweredFunc = temporaries.get(arg.identifier.id); if (maybeLoweredFunc != null) { - hoistableFunctions.add(maybeLoweredFunc.fn); + deferredInvoked.add(maybeLoweredFunc.fn); } } } } } else if (value.kind === 'JsxExpression') { /** - * Assume JSX attributes and children are safe to invoke + * JSX attributes and children are deferred — event handlers like + * onClick don't execute during render. */ for (const attr of value.props) { if (attr.kind === 'JsxSpreadAttribute') { @@ -767,13 +808,13 @@ function getAssumedInvokedFunctions( } const maybeLoweredFunc = temporaries.get(attr.place.identifier.id); if (maybeLoweredFunc != null) { - hoistableFunctions.add(maybeLoweredFunc.fn); + deferredInvoked.add(maybeLoweredFunc.fn); } } for (const child of value.children ?? []) { const maybeLoweredFunc = temporaries.get(child.identifier.id); if (maybeLoweredFunc != null) { - hoistableFunctions.add(maybeLoweredFunc.fn); + deferredInvoked.add(maybeLoweredFunc.fn); } } } else if (value.kind === 'FunctionExpression') { @@ -792,7 +833,10 @@ function getAssumedInvokedFunctions( ); const maybeLoweredFunc = temporaries.get(lvalue.identifier.id); if (maybeLoweredFunc != null) { - for (const called of lambdasCalled) { + for (const called of lambdasCalled.syncInvoked) { + maybeLoweredFunc.mayInvoke.add(called); + } + for (const called of lambdasCalled.deferredInvoked) { maybeLoweredFunc.mayInvoke.add(called); } } @@ -800,23 +844,39 @@ function getAssumedInvokedFunctions( } if (block.terminal.kind === 'return') { /** - * Assume directly returned functions are safe to call + * Returned functions are deferred — they execute in the caller's + * context, not during this render. */ const maybeLoweredFunc = temporaries.get( block.terminal.value.identifier.id, ); if (maybeLoweredFunc != null) { - hoistableFunctions.add(maybeLoweredFunc.fn); + deferredInvoked.add(maybeLoweredFunc.fn); } } } + /** + * Step 3: Transitive closure. If a function is assumed invoked, then all + * functions it may invoke are also assumed invoked. The category (sync vs + * deferred) propagates from the parent: if a sync function calls another + * function, that function is also sync. If a deferred function calls + * another function, that function is also deferred. If a function appears + * in both sets, sync wins (it IS called synchronously via at least one path). + */ for (const [_, {fn, mayInvoke}] of temporaries) { - if (hoistableFunctions.has(fn)) { + if (syncInvoked.has(fn)) { for (const called of mayInvoke) { - hoistableFunctions.add(called); + syncInvoked.add(called); + } + } + if (deferredInvoked.has(fn)) { + for (const called of mayInvoke) { + if (!syncInvoked.has(called)) { + deferredInvoked.add(called); + } } } } - return hoistableFunctions; + return {syncInvoked, deferredInvoked}; } From dcc050349dbb27b930d50de18795cafe2e34516b Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 14:56:43 -0700 Subject: [PATCH 4/7] [Compiler] Update fixture snapshots for sync/deferred split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update 40 fixture snapshots to reflect the new behavior: Bug fixtures (10 files): Cache keys change from non-optional property accesses like `$[0] !== user.name` to truncated object references like `$[0] !== user`. This prevents TypeError when the base object is null/undefined. Correctness guard fixtures (2 files): UNCHANGED — render-time property accesses and direct synchronous calls still correctly use fine-grained cache keys like `$[0] !== obj.value`. Existing assume-invoked fixtures: - direct-call.ts: UNCHANGED (sync — direct call during render) - conditional-call.ts: UNCHANGED (sync — direct call) - jsx-function.tsx: obj.value → obj (JSX prop is deferred) - hook-call.ts: obj.value → obj (custom hook arg is deferred) - return-function.ts: obj.value → obj (return is deferred) - conditional-call-chain.tsx: a.value/b.value → a/b (transitive deferred) Other affected fixtures use the same pattern: property accesses that were only reachable through deferred inner functions now produce coarser-grained but safe cache keys. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...turing-function-member-expr-call.expect.md | 4 +-- .../compiler/capturing-member-expr.expect.md | 20 ++++--------- ...ested-member-expr-in-nested-func.expect.md | 22 +++++--------- .../capturing-nested-member-expr.expect.md | 20 ++++--------- ...and-local-variables-with-default.expect.md | 4 +-- ...ed-scope-declarations-and-locals.expect.md | 4 +-- .../error.ref-like-name-not-Ref.expect.md | 2 +- .../error.ref-like-name-not-a-ref.expect.md | 2 +- ...call-freezes-captured-memberexpr.expect.md | 4 +-- ...map-named-callback-cross-context.expect.md | 4 +-- .../conditional-call-chain.expect.md | 8 ++--- .../conditionally-return-fn.expect.md | 4 +-- ...nal-callsite-in-another-function.expect.md | 4 +-- .../assume-invoked/hook-call.expect.md | 4 +-- .../assume-invoked/jsx-function.expect.md | 4 +-- .../assume-invoked/return-function.expect.md | 4 +-- ...map-named-callback-cross-context.expect.md | 4 +-- ...lback-conditional-access-noAlloc.expect.md | 2 +- .../useCallback-infer-more-specific.expect.md | 4 +-- ...r-function-uncond-access-hoisted.expect.md | 4 +-- ...n-uncond-access-hoists-other-dep.expect.md | 4 +-- ...function-uncond-access-local-var.expect.md | 4 +-- ...uncond-optional-hoists-other-dep.expect.md | 4 +-- ...er-nested-function-uncond-access.expect.md | 4 +-- ...unction-uncond-optionals-hoisted.expect.md | 4 +-- .../repro-invariant.expect.md | 4 +-- ...unction-uncond-optionals-hoisted.expect.md | 4 +-- ...ro-context-var-reassign-no-scope.expect.md | 29 ++++++++----------- ...maybe-mutate-context-in-callback.expect.md | 4 +-- ...Context-read-context-in-callback.expect.md | 4 +-- 30 files changed, 82 insertions(+), 111 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md index cab9c9a500b9..621ad98de7af 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md @@ -35,11 +35,11 @@ function component(t0) { } const poke = t1; let t2; - if ($[2] !== mutator.user) { + if ($[2] !== mutator) { t2 = () => { mutator.user.hide(); }; - $[2] = mutator.user; + $[2] = mutator; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md index 943533a63d12..a51592352b5e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md @@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a }; + const z = { a }; + t0 = function () { + console.log(z.a); + }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const z = t0; - let t1; - if ($[2] !== z.a) { - t1 = function () { - console.log(z.a); - }; - $[2] = z.a; - $[3] = t1; - } else { - t1 = $[3]; - } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr-in-nested-func.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr-in-nested-func.expect.md index 9e95af2b0d59..cbf30a145cb3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr-in-nested-func.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr-in-nested-func.expect.md @@ -25,29 +25,21 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a: { a } }; - $[0] = a; - $[1] = t0; - } else { - t0 = $[1]; - } - const z = t0; - let t1; - if ($[2] !== z.a.a) { - t1 = function () { + const z = { a: { a } }; + t0 = function () { (function () { console.log(z.a.a); })(); }; - $[2] = z.a.a; - $[3] = t1; + $[0] = a; + $[1] = t0; } else { - t1 = $[3]; + t0 = $[1]; } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md index 13c122750e9b..f41a7fae4c9b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md @@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a: { a } }; + const z = { a: { a } }; + t0 = function () { + console.log(z.a.a); + }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const z = t0; - let t1; - if ($[2] !== z.a.a) { - t1 = function () { - console.log(z.a.a); - }; - $[2] = z.a.a; - $[3] = t1; - } else { - t1 = $[3]; - } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md index 3a8f9e84cac1..84f14c026b13 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md @@ -96,14 +96,14 @@ function Component(props) { } const urls = t5; let t6; - if ($[6] !== comments.length) { + if ($[6] !== comments) { t6 = (e) => { if (!comments.length) { return; } console.log(comments.length); }; - $[6] = comments.length; + $[6] = comments; $[7] = t6; } else { t6 = $[7]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md index fbb6e50871dc..046669da02fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md @@ -55,14 +55,14 @@ function Component(props) { const allUrls = []; const { media, comments, urls } = post; let t1; - if ($[2] !== comments.length) { + if ($[2] !== comments) { t1 = (e) => { if (!comments.length) { return; } console.log(comments.length); }; - $[2] = comments.length; + $[2] = comments; $[3] = t1; } else { t1 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md index 2558d10d19cb..6e62d38d906a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md @@ -35,7 +35,7 @@ Found 1 error: Compilation Skipped: Existing memoization could not be preserved -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref.current`, but the source dependencies were []. Inferred dependency not present in source. +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref`, but the source dependencies were []. Inferred dependency not present in source. error.ref-like-name-not-Ref.ts:11:30 9 | const Ref = useCustomRef(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md index 2c2f725ec84b..27f91a19a213 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md @@ -35,7 +35,7 @@ Found 1 error: Compilation Skipped: Existing memoization could not be preserved -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref.current`, but the source dependencies were []. Inferred dependency not present in source. +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref`, but the source dependencies were []. Inferred dependency not present in source. error.ref-like-name-not-a-ref.ts:11:30 9 | const notaref = useCustomRef(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md index 957919516d09..70ca300b0e6b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md @@ -45,9 +45,9 @@ function Foo(t0) { } const x = t1; let t2; - if ($[2] !== x.inner) { + if ($[2] !== x) { t2 = () => x.inner; - $[2] = x.inner; + $[2] = x; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md index c1a6dfb3eae1..864c16a9d626 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md @@ -60,9 +60,9 @@ function useFoo(t0) { const $ = _c(13); const { arr1, arr2 } = t0; let t1; - if ($[0] !== arr1[0]) { + if ($[0] !== arr1) { t1 = (e) => arr1[0].value + e.value; - $[0] = arr1[0]; + $[0] = arr1; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md index 4622beeb0eb8..f307087325cb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md @@ -45,22 +45,22 @@ function Component(t0) { const $ = _c(7); const { a, b } = t0; let t1; - if ($[0] !== a.value) { + if ($[0] !== a) { t1 = () => { console.log(a.value); }; - $[0] = a.value; + $[0] = a; $[1] = t1; } else { t1 = $[1]; } const logA = t1; let t2; - if ($[2] !== b.value) { + if ($[2] !== b) { t2 = () => { console.log(b.value); }; - $[2] = b.value; + $[2] = b; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md index 77b62bc8c24b..933f5d6e9743 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md @@ -51,9 +51,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, shouldMakeCb, setState } = t0; let t1; - if ($[0] !== obj.value || $[1] !== setState) { + if ($[0] !== obj || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj.value; + $[0] = obj; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md index 8301912b024c..6663da19e2d4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md @@ -89,9 +89,9 @@ function useMakeCallback(t0) { const $ = _c(6); const { obj, cond, setState } = t0; let t1; - if ($[0] !== obj.value || $[1] !== setState) { + if ($[0] !== obj || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj.value; + $[0] = obj; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md index ab8326a2286d..12a74509539a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md @@ -48,9 +48,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, setState } = t0; let t1; - if ($[0] !== obj.value || $[1] !== setState) { + if ($[0] !== obj || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj.value; + $[0] = obj; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md index 76228fc24911..06d56f48b676 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md @@ -44,9 +44,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, setState } = t0; let t1; - if ($[0] !== obj.value || $[1] !== setState) { + if ($[0] !== obj || $[1] !== setState) { t1 = setState(obj.value)} shouldInvokeFns={true} />; - $[0] = obj.value; + $[0] = obj; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md index 31e317d07e99..8e40997e3366 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md @@ -47,9 +47,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, setState } = t0; let t1; - if ($[0] !== obj.value || $[1] !== setState) { + if ($[0] !== obj || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj.value; + $[0] = obj; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md index 7bc2c193cf7c..671dcb539c57 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md @@ -61,9 +61,9 @@ function useFoo(t0) { const $ = _c(13); const { arr1, arr2 } = t0; let t1; - if ($[0] !== arr1[0]) { + if ($[0] !== arr1) { t1 = (e) => arr1[0].value + e.value; - $[0] = arr1[0]; + $[0] = arr1; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md index 075458831b8d..0aa4318090d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md @@ -29,7 +29,7 @@ Found 1 error: Compilation Skipped: Existing memoization could not be preserved -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB?.x.y`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source. +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB?.x`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source. error.useCallback-conditional-access-noAlloc.ts:5:21 3 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md index 7e57f294927a..5c2192e35da9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md @@ -37,9 +37,9 @@ import { useCallback } from "react"; function useHook(x) { const $ = _c(2); let t0; - if ($[0] !== x.y.z) { + if ($[0] !== x) { t0 = () => [x.y.z]; - $[0] = x.y.z; + $[0] = x; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md index 1ddc7495bc3e..9ddead2734c5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md @@ -29,9 +29,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a.b.c) { + if ($[0] !== a) { t1 = a.b.c} shouldInvokeFns={true} />; - $[0] = a.b.c; + $[0] = a; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md index d82956e4a0db..eab9491c4730 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md @@ -51,12 +51,12 @@ function Foo(t0) { const fn = t1; useIdentity(null); let x; - if ($[2] !== a.b.c || $[3] !== cond) { + if ($[2] !== a || $[3] !== cond) { x = makeArray(); if (cond) { x.push(identity(a.b.c)); } - $[2] = a.b.c; + $[2] = a; $[3] = cond; $[4] = x; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md index 41bab7ccc9d6..dd52d90b4572 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md @@ -41,10 +41,10 @@ function useFoo(t0) { local = $[1]; } let t1; - if ($[2] !== local.b.c) { + if ($[2] !== local) { const fn = () => local.b.c; t1 = ; - $[2] = local.b.c; + $[2] = local; $[3] = t1; } else { t1 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md index c81e59eceaf9..7d8c1b32db87 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md @@ -50,12 +50,12 @@ function Foo(t0) { const fn = t1; useIdentity(null); let arr; - if ($[2] !== a.b?.c.e || $[3] !== cond) { + if ($[2] !== a || $[3] !== cond) { arr = makeArray(); if (cond) { arr.push(identity(a.b?.c.e)); } - $[2] = a.b?.c.e; + $[2] = a; $[3] = cond; $[4] = arr; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md index 77f5ae4db3c4..443b1863bd37 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md @@ -34,10 +34,10 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a.b.c) { + if ($[0] !== a) { const fn = () => () => ({ value: a.b.c }); t1 = ; - $[0] = a.b.c; + $[0] = a; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md index ed56ff068113..883d4dc8d687 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md @@ -34,9 +34,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a.b?.c.d?.e) { + if ($[0] !== a) { t1 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a.b?.c.d?.e; + $[0] = a; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md index 73df2b615b9e..cdb9d67e361c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md @@ -29,9 +29,9 @@ function Foo(t0) { const $ = _c(5); const { data } = t0; let t1; - if ($[0] !== data.a.d) { + if ($[0] !== data.a) { t1 = () => data.a.d; - $[0] = data.a.d; + $[0] = data.a; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md index bb99a5d90fe2..1c65af0fe6ca 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md @@ -31,9 +31,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a.b?.c.d?.e) { + if ($[0] !== a) { t1 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a.b?.c.d?.e; + $[0] = a; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md index cacd56492214..3b21db2328ab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md @@ -44,7 +44,7 @@ import { useState, useEffect } from "react"; import { invoke, Stringify } from "shared-runtime"; function Content() { - const $ = _c(8); + const $ = _c(7); const [announcement, setAnnouncement] = useState(""); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { @@ -55,7 +55,8 @@ function Content() { } const [users, setUsers] = useState(t0); let t1; - if ($[1] !== users.length) { + let t2; + if ($[1] !== users) { t1 = () => { if (users.length === 2) { let removedUserName = ""; @@ -65,32 +66,26 @@ function Content() { newUsers.pop(); return newUsers; }); - setAnnouncement(`Removed user (${removedUserName})`); } }; - $[1] = users.length; + t2 = [users]; + $[1] = users; $[2] = t1; + $[3] = t2; } else { t1 = $[2]; - } - let t2; - if ($[3] !== users) { - t2 = [users]; - $[3] = users; - $[4] = t2; - } else { - t2 = $[4]; + t2 = $[3]; } useEffect(t1, t2); let t3; - if ($[5] !== announcement || $[6] !== users) { + if ($[4] !== announcement || $[5] !== users) { t3 = ; - $[5] = announcement; - $[6] = users; - $[7] = t3; + $[4] = announcement; + $[5] = users; + $[6] = t3; } else { - t3 = $[7]; + t3 = $[6]; } return t3; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md index 0525a0f7cdd9..117031695888 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md @@ -41,11 +41,11 @@ function Component(props) { const $ = _c(5); const Foo = useContext(FooContext); let t0; - if ($[0] !== Foo.current) { + if ($[0] !== Foo) { t0 = () => { mutate(Foo.current); }; - $[0] = Foo.current; + $[0] = Foo; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md index e805b7f400e3..09dbbc230f3b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md @@ -34,11 +34,11 @@ function Component(props) { const $ = _c(5); const foo = useContext(FooContext); let t0; - if ($[0] !== foo.current) { + if ($[0] !== foo) { t0 = () => { console.log(foo.current); }; - $[0] = foo.current; + $[0] = foo; $[1] = t0; } else { t0 = $[1]; From 7e20eea3f6e29eb22f3d18bdc9877f45ee7a0419 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 15:00:33 -0700 Subject: [PATCH 5/7] [Compiler] Update bug fixture snapshots to reflect the fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the 10 bug reproduction fixture snapshots to show the corrected compiler output. The cache key comparisons now use safe object-level references instead of crashing property accesses: Before (buggy): $[0] !== user.name → TypeError when user is null After (fixed): $[0] !== user → safe comparison This confirms all 10 bug patterns are resolved by the sync/deferred split in getAssumedInvokedFunctions. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...bug-hoisted-nullable-closure-assert-function.expect.md | 4 ++-- ...d-nullable-closure-conditional-render-access.expect.md | 4 ++-- .../bug-hoisted-nullable-closure-hook-argument.expect.md | 4 ++-- ...oisted-nullable-closure-mixed-optional-chain.expect.md | 4 ++-- ...g-hoisted-nullable-closure-multiple-closures.expect.md | 8 ++++---- ...bug-hoisted-nullable-closure-nested-property.expect.md | 4 ++-- ...bug-hoisted-nullable-closure-property-access.expect.md | 4 ++-- ...g-hoisted-nullable-closure-returned-function.expect.md | 4 ++-- ...isted-nullable-closure-ts-non-null-assertion.expect.md | 4 ++-- ...ug-hoisted-nullable-closure-use-effect-event.expect.md | 4 ++-- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md index 9846e313482d..9296695f0c60 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md @@ -60,13 +60,13 @@ function Component(t0) { const $ = _c(2); const { planPeriod } = t0; let t1; - if ($[0] !== planPeriod.id) { + if ($[0] !== planPeriod) { const callback = () => { assertIsNotEmpty(planPeriod?.id); console.log(planPeriod.id); }; t1 = ; - $[0] = planPeriod.id; + $[0] = planPeriod; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md index a21e5b14dade..9b4131886a1a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md @@ -50,11 +50,11 @@ function Component(t0) { const $ = _c(5); const { user } = t0; let t1; - if ($[0] !== user.email) { + if ($[0] !== user) { t1 = () => { console.log(user.email); }; - $[0] = user.email; + $[0] = user; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md index 4e921cb0dbd3..dd847800e880 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md @@ -45,9 +45,9 @@ function Component(t0) { const $ = _c(4); const { item } = t0; let t1; - if ($[0] !== item.id) { + if ($[0] !== item) { t1 = () => item.id; - $[0] = item.id; + $[0] = item; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md index 68f356b2c6ac..6183a922a002 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md @@ -46,12 +46,12 @@ function Component(t0) { const $ = _c(2); const { user } = t0; let t1; - if ($[0] !== user?.company.name) { + if ($[0] !== user?.company) { const handleClick = () => { console.log(user?.company.name); }; t1 = Click; - $[0] = user?.company.name; + $[0] = user?.company; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md index bcadac93b56b..0d4ad43b6b40 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md @@ -59,22 +59,22 @@ function Component(t0) { const $ = _c(9); const { user, post } = t0; let t1; - if ($[0] !== user.name) { + if ($[0] !== user) { t1 = () => { console.log(user.name); }; - $[0] = user.name; + $[0] = user; $[1] = t1; } else { t1 = $[1]; } const handleUser = t1; let t2; - if ($[2] !== post.title) { + if ($[2] !== post) { t2 = () => { console.log(post.title); }; - $[2] = post.title; + $[2] = post; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md index 2d4cdcff7523..79dcb3c13851 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md @@ -53,11 +53,11 @@ function Component(t0) { const $ = _c(5); const { post } = t0; let t1; - if ($[0] !== post.author.profile.avatar) { + if ($[0] !== post) { t1 = () => { console.log(post.author.profile.avatar); }; - $[0] = post.author.profile.avatar; + $[0] = post; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md index 2167367abf55..79842ca95c80 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md @@ -50,11 +50,11 @@ function Component(t0) { const $ = _c(5); const { user } = t0; let t1; - if ($[0] !== user.name) { + if ($[0] !== user) { t1 = () => { console.log(user.name); }; - $[0] = user.name; + $[0] = user; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md index e332c5337bfa..63e8855e8cc4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md @@ -43,11 +43,11 @@ function useHandler(t0) { const $ = _c(2); const { item } = t0; let t1; - if ($[0] !== item.id) { + if ($[0] !== item) { t1 = () => { console.log(item.id); }; - $[0] = item.id; + $[0] = item; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md index d159106cc2a5..4f4ecf9c4267 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md @@ -44,11 +44,11 @@ function Component(t0) { const $ = _c(5); const { data } = t0; let t1; - if ($[0] !== data.id) { + if ($[0] !== data) { t1 = () => { console.log(data.id); }; - $[0] = data.id; + $[0] = data; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md index 3f6f2bcfdcd4..da9cfbec90db 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md @@ -41,11 +41,11 @@ function Component(t0) { const $ = _c(5); const { data } = t0; let t1; - if ($[0] !== data.value) { + if ($[0] !== data) { t1 = () => { console.log(data.value); }; - $[0] = data.value; + $[0] = data; $[1] = t1; } else { t1 = $[1]; From 875803495e2344d3784bd0f66ed5e89f77a938a9 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 17:03:03 -0700 Subject: [PATCH 6/7] [Compiler] Preserve fine-grained cache keys via optional access for deferred closures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of truncating dependency paths to the base object when properties aren't provably non-null (e.g. `user.name` → `user`), emit optional access in cache keys (e.g. `user?.name`). This preserves value-level memoization granularity while remaining safe for nullable objects. The key change is threading deferred closure property paths as a separate set (`deferredNonNullObjects`) through the hoistable property load analysis. When `addDependency()` in `DeriveMinimalDependenciesHIR` would normally truncate a path because the base isn't in the main hoistable tree, it now checks the deferred tree as a fallback and emits optional access instead of breaking. This is scoped to only deferred closures — render-time truncation (e.g. try-catch, jump-poisoned scopes) is preserved because those paths aren't in the deferred tree. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/HIR/CollectHoistablePropertyLoads.ts | 43 ++++++++++-- .../src/HIR/DeriveMinimalDependenciesHIR.ts | 70 +++++++++++++++++-- .../src/HIR/PropagateScopeDependenciesHIR.ts | 1 + 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts index 19bac9b39d73..eb862de2221c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -221,6 +221,13 @@ export function keyByScopeId( export type BlockInfo = { block: BasicBlock; assumedNonNullObjects: ReadonlySet; + /** + * Property paths from deferred closures (JSX event handlers, hook callbacks, + * returned functions). These cannot prove non-nullness during render, but are + * tracked separately so that downstream cache key generation can use optional + * access (e.g. `user?.name`) instead of truncating to just the base object. + */ + deferredNonNullObjects: ReadonlySet; }; /** @@ -418,12 +425,14 @@ function collectNonNullsInBlocks( { block: BasicBlock; assumedNonNullObjects: Set; + deferredNonNullObjects: Set; } >(); for (const [_, block] of fn.body.blocks) { const assumedNonNullObjects = new Set( knownNonNullIdentifiers, ); + const deferredNonNullObjects = new Set(); const maybeOptionalChain = context.hoistableFromOptionals.get(block.id); if (maybeOptionalChain != null) { @@ -471,13 +480,17 @@ function collectNonNullsInBlocks( * See: https://github.com/facebook/react/issues/34752 * https://github.com/facebook/react/issues/35762 */ + const innerHoistables = assertNonNull( + innerHoistableMap.get(innerFn.func.body.entry), + ); if (isSyncInvoked) { - const innerHoistables = assertNonNull( - innerHoistableMap.get(innerFn.func.body.entry), - ); for (const entry of innerHoistables.assumedNonNullObjects) { assumedNonNullObjects.add(entry); } + } else if (isDeferredInvoked) { + for (const entry of innerHoistables.assumedNonNullObjects) { + deferredNonNullObjects.add(entry); + } } } } else if ( @@ -513,6 +526,7 @@ function collectNonNullsInBlocks( nodes.set(block.id, { block, assumedNonNullObjects, + deferredNonNullObjects, }); } return nodes; @@ -599,10 +613,11 @@ function propagateNonNull( * it's not safe to assume they can be filtered out (e.g. not included in * the intersection) */ + const doneNeighbors = Array.from(neighbors).filter( + n => traversalState.get(n) === 'done', + ); const neighborAccesses = Set_intersect( - Array.from(neighbors) - .filter(n => traversalState.get(n) === 'done') - .map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects), + doneNeighbors.map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects), ); const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects; @@ -610,6 +625,18 @@ function propagateNonNull( reduceMaybeOptionalChains(mergedObjects, registry); assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects; + + // Also propagate deferred non-null objects + const neighborDeferred = Set_intersect( + doneNeighbors.map( + n => assertNonNull(nodes.get(n)).deferredNonNullObjects, + ), + ); + const prevDeferred = + assertNonNull(nodes.get(nodeId)).deferredNonNullObjects; + const mergedDeferred = Set_union(prevDeferred, neighborDeferred); + assertNonNull(nodes.get(nodeId)).deferredNonNullObjects = mergedDeferred; + traversalState.set(nodeId, 'done'); /** * Note that it's not sufficient to compare set sizes since @@ -617,7 +644,9 @@ function propagateNonNull( * unconditional loads. This could in turn change `assumedNonNullObjects` of * downstream blocks and backedges. */ - changed ||= !Set_equal(prevObjects, mergedObjects); + changed ||= + !Set_equal(prevObjects, mergedObjects) || + !Set_equal(prevDeferred, mergedDeferred); return changed; } const traversalState = new Map(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts index 2850e73ca5a0..b56a460c530c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts @@ -28,6 +28,16 @@ export class ReactiveScopeDependencyTreeHIR { */ #hoistableObjects: Map = new Map(); + /** + * Paths from deferred closures (event handlers, hook callbacks, returned + * functions). These cannot prove non-nullness at render time, but can be + * used to preserve fine-grained cache keys via optional access. When a + * dependency path would normally be truncated, we check this tree as a + * fallback — if the path exists here, we emit optional access (e.g. + * `user?.name`) instead of truncating to just `user`. + */ + #deferredObjects: Map = + new Map(); #deps: Map = new Map(); /** @@ -35,13 +45,34 @@ export class ReactiveScopeDependencyTreeHIR { * PropertyLoads. Note that we expect these to not contain duplicates (e.g. * both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges * duplicates when traversing the CFG. + * @param deferredObjects a set of property paths from deferred closures, + * used as a fallback for optional cache key generation. */ - constructor(hoistableObjects: Iterable) { - for (const {path, identifier, reactive, loc} of hoistableObjects) { + constructor( + hoistableObjects: Iterable, + deferredObjects?: Iterable, + ) { + ReactiveScopeDependencyTreeHIR.#buildTree( + hoistableObjects, + this.#hoistableObjects, + ); + if (deferredObjects != null) { + ReactiveScopeDependencyTreeHIR.#buildTree( + deferredObjects, + this.#deferredObjects, + ); + } + } + + static #buildTree( + objects: Iterable, + tree: Map, + ): void { + for (const {path, identifier, reactive, loc} of objects) { let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot( identifier, reactive, - this.#hoistableObjects, + tree, path.length > 0 && path[0].optional ? 'Optional' : 'NonNull', loc, ); @@ -122,10 +153,18 @@ export class ReactiveScopeDependencyTreeHIR { */ let hoistableCursor: HoistableNode | undefined = this.#hoistableObjects.get(identifier); + /** + * deferredCursor tracks the same path in the deferred objects tree. + * When the main hoistable tree doesn't have a path (would truncate), + * we check the deferred tree as a fallback to emit optional access. + */ + let deferredCursor: HoistableNode | undefined = + this.#deferredObjects.get(identifier); // All properties read 'on the way' to a dependency are marked as 'access' for (const entry of path) { let nextHoistableCursor: HoistableNode | undefined; + let nextDeferredCursor: HoistableNode | undefined; let nextDepCursor: DependencyNode; if (entry.optional) { /** @@ -136,6 +175,9 @@ export class ReactiveScopeDependencyTreeHIR { if (hoistableCursor != null) { nextHoistableCursor = hoistableCursor?.properties.get(entry.property); } + if (deferredCursor != null) { + nextDeferredCursor = deferredCursor.properties.get(entry.property); + } let accessType; if ( @@ -166,20 +208,40 @@ export class ReactiveScopeDependencyTreeHIR { hoistableCursor.accessType === 'NonNull' ) { nextHoistableCursor = hoistableCursor.properties.get(entry.property); + if (deferredCursor != null) { + nextDeferredCursor = deferredCursor.properties.get(entry.property); + } nextDepCursor = makeOrMergeProperty( depCursor, entry.property, PropertyAccessType.UnconditionalAccess, entry.loc, ); + } else if (deferredCursor != null) { + /** + * The main hoistable tree would truncate here, but the deferred + * tree has this path — emit optional access to preserve fine-grained + * cache keys. For example, `user.name` inside an onClick handler + * becomes `user?.name` in the cache key instead of truncating to + * just `user`. + */ + nextDeferredCursor = deferredCursor.properties.get(entry.property); + nextDepCursor = makeOrMergeProperty( + depCursor, + entry.property, + PropertyAccessType.OptionalAccess, + entry.loc, + ); } else { /** - * Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from + * Break to truncate the dependency on its first non-optional entry + * that PropertyLoads are not hoistable from */ break; } depCursor = nextDepCursor; hoistableCursor = nextHoistableCursor; + deferredCursor = nextDeferredCursor; } // mark the final node as a dependency depCursor.accessType = merge( diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts index 19b4fae30fda..9f5ae400fcab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -94,6 +94,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void { */ const tree = new ReactiveScopeDependencyTreeHIR( [...hoistables.assumedNonNullObjects].map(o => o.fullPath), + [...hoistables.deferredNonNullObjects].map(o => o.fullPath), ); for (const dep of deps) { tree.addDependency({...dep}); From 4e7e09bc2663c7c41a8bb9d51c37855cf68014d1 Mon Sep 17 00:00:00 2001 From: Jarred Stelfox Date: Fri, 20 Mar 2026 17:03:22 -0700 Subject: [PATCH 7/7] [Compiler] Update fixture snapshots for optional cache key improvement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache keys now use optional access (e.g. `user?.name`) instead of truncating to the base object (`user`) for deferred closure dependencies. This provides finer-grained memoization — cache invalidation tracks specific property values rather than object identity. Key snapshot changes: - Bug fixtures: `user` → `user?.name`, `post` → `post?.author?.profile?.avatar` - Existing assume-invoked fixtures: `obj` → `obj?.value` for deferred paths - Sync paths (direct-call, conditional-call): unchanged Co-Authored-By: Claude Opus 4.6 (1M context) --- ...nullable-closure-assert-function.expect.md | 4 +-- ...losure-conditional-render-access.expect.md | 4 +-- ...d-nullable-closure-hook-argument.expect.md | 4 +-- ...ble-closure-mixed-optional-chain.expect.md | 4 +-- ...llable-closure-multiple-closures.expect.md | 8 ++--- ...nullable-closure-nested-property.expect.md | 4 +-- ...nullable-closure-property-access.expect.md | 4 +-- ...llable-closure-returned-function.expect.md | 4 +-- ...le-closure-ts-non-null-assertion.expect.md | 4 +-- ...ullable-closure-use-effect-event.expect.md | 4 +-- ...turing-function-member-expr-call.expect.md | 4 +-- .../compiler/capturing-member-expr.expect.md | 20 +++++++++---- .../capturing-nested-member-expr.expect.md | 20 +++++++++---- ...and-local-variables-with-default.expect.md | 4 +-- ...ed-scope-declarations-and-locals.expect.md | 4 +-- .../error.ref-like-name-not-Ref.expect.md | 2 +- .../error.ref-like-name-not-a-ref.expect.md | 2 +- ...call-freezes-captured-memberexpr.expect.md | 4 +-- ...map-named-callback-cross-context.expect.md | 4 +-- .../conditional-call-chain.expect.md | 8 ++--- .../conditionally-return-fn.expect.md | 4 +-- ...nal-callsite-in-another-function.expect.md | 4 +-- .../assume-invoked/hook-call.expect.md | 4 +-- .../assume-invoked/jsx-function.expect.md | 4 +-- .../assume-invoked/return-function.expect.md | 4 +-- ...map-named-callback-cross-context.expect.md | 4 +-- ...lback-conditional-access-noAlloc.expect.md | 2 +- .../useCallback-infer-more-specific.expect.md | 4 +-- ...r-function-uncond-access-hoisted.expect.md | 4 +-- ...n-uncond-access-hoists-other-dep.expect.md | 4 +-- ...function-uncond-access-local-var.expect.md | 4 +-- ...uncond-optional-hoists-other-dep.expect.md | 4 +-- ...unction-uncond-optionals-hoisted.expect.md | 4 +-- .../repro-invariant.expect.md | 4 +-- ...unction-uncond-optionals-hoisted.expect.md | 4 +-- ...ro-context-var-reassign-no-scope.expect.md | 29 +++++++++++-------- ...maybe-mutate-context-in-callback.expect.md | 4 +-- ...Context-read-context-in-callback.expect.md | 4 +-- 38 files changed, 116 insertions(+), 95 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md index 9296695f0c60..84107ecad0d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-assert-function.expect.md @@ -60,13 +60,13 @@ function Component(t0) { const $ = _c(2); const { planPeriod } = t0; let t1; - if ($[0] !== planPeriod) { + if ($[0] !== planPeriod?.id) { const callback = () => { assertIsNotEmpty(planPeriod?.id); console.log(planPeriod.id); }; t1 = ; - $[0] = planPeriod; + $[0] = planPeriod?.id; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md index 9b4131886a1a..cc380ec15d80 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-conditional-render-access.expect.md @@ -50,11 +50,11 @@ function Component(t0) { const $ = _c(5); const { user } = t0; let t1; - if ($[0] !== user) { + if ($[0] !== user?.email) { t1 = () => { console.log(user.email); }; - $[0] = user; + $[0] = user?.email; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md index dd847800e880..34c4736ee850 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-hook-argument.expect.md @@ -45,9 +45,9 @@ function Component(t0) { const $ = _c(4); const { item } = t0; let t1; - if ($[0] !== item) { + if ($[0] !== item?.id) { t1 = () => item.id; - $[0] = item; + $[0] = item?.id; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md index 6183a922a002..ad917b15b935 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-mixed-optional-chain.expect.md @@ -46,12 +46,12 @@ function Component(t0) { const $ = _c(2); const { user } = t0; let t1; - if ($[0] !== user?.company) { + if ($[0] !== user?.company?.name) { const handleClick = () => { console.log(user?.company.name); }; t1 = Click; - $[0] = user?.company; + $[0] = user?.company?.name; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md index 0d4ad43b6b40..7cdc1f705710 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-multiple-closures.expect.md @@ -59,22 +59,22 @@ function Component(t0) { const $ = _c(9); const { user, post } = t0; let t1; - if ($[0] !== user) { + if ($[0] !== user?.name) { t1 = () => { console.log(user.name); }; - $[0] = user; + $[0] = user?.name; $[1] = t1; } else { t1 = $[1]; } const handleUser = t1; let t2; - if ($[2] !== post) { + if ($[2] !== post?.title) { t2 = () => { console.log(post.title); }; - $[2] = post; + $[2] = post?.title; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md index 79dcb3c13851..c000a02e3986 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-nested-property.expect.md @@ -53,11 +53,11 @@ function Component(t0) { const $ = _c(5); const { post } = t0; let t1; - if ($[0] !== post) { + if ($[0] !== post?.author?.profile?.avatar) { t1 = () => { console.log(post.author.profile.avatar); }; - $[0] = post; + $[0] = post?.author?.profile?.avatar; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md index 79842ca95c80..cff3e41790b7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-property-access.expect.md @@ -50,11 +50,11 @@ function Component(t0) { const $ = _c(5); const { user } = t0; let t1; - if ($[0] !== user) { + if ($[0] !== user?.name) { t1 = () => { console.log(user.name); }; - $[0] = user; + $[0] = user?.name; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md index 63e8855e8cc4..f2ab2407a906 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-returned-function.expect.md @@ -43,11 +43,11 @@ function useHandler(t0) { const $ = _c(2); const { item } = t0; let t1; - if ($[0] !== item) { + if ($[0] !== item?.id) { t1 = () => { console.log(item.id); }; - $[0] = item; + $[0] = item?.id; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md index 4f4ecf9c4267..35bc8b9cdcfc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-ts-non-null-assertion.expect.md @@ -44,11 +44,11 @@ function Component(t0) { const $ = _c(5); const { data } = t0; let t1; - if ($[0] !== data) { + if ($[0] !== data?.id) { t1 = () => { console.log(data.id); }; - $[0] = data; + $[0] = data?.id; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md index da9cfbec90db..8cba5adf57ab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-hoisted-nullable-closure-use-effect-event.expect.md @@ -41,11 +41,11 @@ function Component(t0) { const $ = _c(5); const { data } = t0; let t1; - if ($[0] !== data) { + if ($[0] !== data?.value) { t1 = () => { console.log(data.value); }; - $[0] = data; + $[0] = data?.value; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md index 621ad98de7af..04192e43a7f5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-member-expr-call.expect.md @@ -35,11 +35,11 @@ function component(t0) { } const poke = t1; let t2; - if ($[2] !== mutator) { + if ($[2] !== mutator?.user) { t2 = () => { mutator.user.hide(); }; - $[2] = mutator; + $[2] = mutator?.user; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md index a51592352b5e..3e34766ee08f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-member-expr.expect.md @@ -23,19 +23,27 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(2); + const $ = _c(4); let t0; if ($[0] !== a) { - const z = { a }; - t0 = function () { - console.log(z.a); - }; + t0 = { a }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const x = t0; + const z = t0; + let t1; + if ($[2] !== z?.a) { + t1 = function () { + console.log(z.a); + }; + $[2] = z?.a; + $[3] = t1; + } else { + t1 = $[3]; + } + const x = t1; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md index f41a7fae4c9b..0817c3d6f36c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-nested-member-expr.expect.md @@ -23,19 +23,27 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(2); + const $ = _c(4); let t0; if ($[0] !== a) { - const z = { a: { a } }; - t0 = function () { - console.log(z.a.a); - }; + t0 = { a: { a } }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const x = t0; + const z = t0; + let t1; + if ($[2] !== z?.a?.a) { + t1 = function () { + console.log(z.a.a); + }; + $[2] = z?.a?.a; + $[3] = t1; + } else { + t1 = $[3]; + } + const x = t1; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md index 84f14c026b13..31c7893f4487 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md @@ -96,14 +96,14 @@ function Component(props) { } const urls = t5; let t6; - if ($[6] !== comments) { + if ($[6] !== comments?.length) { t6 = (e) => { if (!comments.length) { return; } console.log(comments.length); }; - $[6] = comments; + $[6] = comments?.length; $[7] = t6; } else { t6 = $[7]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md index 046669da02fe..3360794ecd04 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md @@ -55,14 +55,14 @@ function Component(props) { const allUrls = []; const { media, comments, urls } = post; let t1; - if ($[2] !== comments) { + if ($[2] !== comments?.length) { t1 = (e) => { if (!comments.length) { return; } console.log(comments.length); }; - $[2] = comments; + $[2] = comments?.length; $[3] = t1; } else { t1 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md index 6e62d38d906a..06695c3b7fa3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-Ref.expect.md @@ -35,7 +35,7 @@ Found 1 error: Compilation Skipped: Existing memoization could not be preserved -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref`, but the source dependencies were []. Inferred dependency not present in source. +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `Ref?.current`, but the source dependencies were []. Inferred dependency not present in source. error.ref-like-name-not-Ref.ts:11:30 9 | const Ref = useCustomRef(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md index 27f91a19a213..642cad49f081 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-like-name-not-a-ref.expect.md @@ -35,7 +35,7 @@ Found 1 error: Compilation Skipped: Existing memoization could not be preserved -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref`, but the source dependencies were []. Inferred dependency not present in source. +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `notaref?.current`, but the source dependencies were []. Inferred dependency not present in source. error.ref-like-name-not-a-ref.ts:11:30 9 | const notaref = useCustomRef(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md index 70ca300b0e6b..4c6e9c0f14d5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md @@ -45,9 +45,9 @@ function Foo(t0) { } const x = t1; let t2; - if ($[2] !== x) { + if ($[2] !== x?.inner) { t2 = () => x.inner; - $[2] = x; + $[2] = x?.inner; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md index 864c16a9d626..778fbcab8ec4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/array-map-named-callback-cross-context.expect.md @@ -60,9 +60,9 @@ function useFoo(t0) { const $ = _c(13); const { arr1, arr2 } = t0; let t1; - if ($[0] !== arr1) { + if ($[0] !== arr1?.[0]) { t1 = (e) => arr1[0].value + e.value; - $[0] = arr1; + $[0] = arr1?.[0]; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md index f307087325cb..ce2c33a22356 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditional-call-chain.expect.md @@ -45,22 +45,22 @@ function Component(t0) { const $ = _c(7); const { a, b } = t0; let t1; - if ($[0] !== a) { + if ($[0] !== a?.value) { t1 = () => { console.log(a.value); }; - $[0] = a; + $[0] = a?.value; $[1] = t1; } else { t1 = $[1]; } const logA = t1; let t2; - if ($[2] !== b) { + if ($[2] !== b?.value) { t2 = () => { console.log(b.value); }; - $[2] = b; + $[2] = b?.value; $[3] = t2; } else { t2 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md index 933f5d6e9743..511e4749f198 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/conditionally-return-fn.expect.md @@ -51,9 +51,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, shouldMakeCb, setState } = t0; let t1; - if ($[0] !== obj || $[1] !== setState) { + if ($[0] !== obj?.value || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj; + $[0] = obj?.value; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md index 6663da19e2d4..de72dba2c14a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/function-with-conditional-callsite-in-another-function.expect.md @@ -89,9 +89,9 @@ function useMakeCallback(t0) { const $ = _c(6); const { obj, cond, setState } = t0; let t1; - if ($[0] !== obj || $[1] !== setState) { + if ($[0] !== obj?.value || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj; + $[0] = obj?.value; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md index 12a74509539a..fc48b4e5e528 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/hook-call.expect.md @@ -48,9 +48,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, setState } = t0; let t1; - if ($[0] !== obj || $[1] !== setState) { + if ($[0] !== obj?.value || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj; + $[0] = obj?.value; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md index 06d56f48b676..94d905ebd29f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/jsx-function.expect.md @@ -44,9 +44,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, setState } = t0; let t1; - if ($[0] !== obj || $[1] !== setState) { + if ($[0] !== obj?.value || $[1] !== setState) { t1 = setState(obj.value)} shouldInvokeFns={true} />; - $[0] = obj; + $[0] = obj?.value; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md index 8e40997e3366..3c6767ad25b8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/return-function.expect.md @@ -47,9 +47,9 @@ function useMakeCallback(t0) { const $ = _c(3); const { obj, setState } = t0; let t1; - if ($[0] !== obj || $[1] !== setState) { + if ($[0] !== obj?.value || $[1] !== setState) { t1 = () => setState(obj.value); - $[0] = obj; + $[0] = obj?.value; $[1] = setState; $[2] = t1; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md index 671dcb539c57..4300b06a281c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md @@ -61,9 +61,9 @@ function useFoo(t0) { const $ = _c(13); const { arr1, arr2 } = t0; let t1; - if ($[0] !== arr1) { + if ($[0] !== arr1?.[0]) { t1 = (e) => arr1[0].value + e.value; - $[0] = arr1; + $[0] = arr1?.[0]; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md index 0aa4318090d9..150efdc3bc23 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-conditional-access-noAlloc.expect.md @@ -29,7 +29,7 @@ Found 1 error: Compilation Skipped: Existing memoization could not be preserved -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB?.x`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source. +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `propB?.x?.y`, but the source dependencies were [propA, propB.x.y]. Inferred different dependency than source. error.useCallback-conditional-access-noAlloc.ts:5:21 3 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md index 5c2192e35da9..69e4b30f7cc5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md @@ -37,9 +37,9 @@ import { useCallback } from "react"; function useHook(x) { const $ = _c(2); let t0; - if ($[0] !== x) { + if ($[0] !== x?.y?.z) { t0 = () => [x.y.z]; - $[0] = x; + $[0] = x?.y?.z; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md index 9ddead2734c5..d36e07616d39 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md @@ -29,9 +29,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a) { + if ($[0] !== a?.b?.c) { t1 = a.b.c} shouldInvokeFns={true} />; - $[0] = a; + $[0] = a?.b?.c; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md index eab9491c4730..a06e78af4304 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md @@ -51,12 +51,12 @@ function Foo(t0) { const fn = t1; useIdentity(null); let x; - if ($[2] !== a || $[3] !== cond) { + if ($[2] !== a?.b?.c || $[3] !== cond) { x = makeArray(); if (cond) { x.push(identity(a.b.c)); } - $[2] = a; + $[2] = a?.b?.c; $[3] = cond; $[4] = x; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md index dd52d90b4572..b632c964d389 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md @@ -41,10 +41,10 @@ function useFoo(t0) { local = $[1]; } let t1; - if ($[2] !== local) { + if ($[2] !== local?.b?.c) { const fn = () => local.b.c; t1 = ; - $[2] = local; + $[2] = local?.b?.c; $[3] = t1; } else { t1 = $[3]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md index 7d8c1b32db87..e3e805f362ca 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md @@ -50,12 +50,12 @@ function Foo(t0) { const fn = t1; useIdentity(null); let arr; - if ($[2] !== a || $[3] !== cond) { + if ($[2] !== a?.b?.c?.e || $[3] !== cond) { arr = makeArray(); if (cond) { arr.push(identity(a.b?.c.e)); } - $[2] = a; + $[2] = a?.b?.c?.e; $[3] = cond; $[4] = arr; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md index 883d4dc8d687..5e36a1f60f36 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md @@ -34,9 +34,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a) { + if ($[0] !== a?.b?.c?.d?.e) { t1 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a; + $[0] = a?.b?.c?.d?.e; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md index cdb9d67e361c..7d6507eaf11f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md @@ -29,9 +29,9 @@ function Foo(t0) { const $ = _c(5); const { data } = t0; let t1; - if ($[0] !== data.a) { + if ($[0] !== data.a?.d) { t1 = () => data.a.d; - $[0] = data.a; + $[0] = data.a?.d; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md index 1c65af0fe6ca..bd091f9e334b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md @@ -31,9 +31,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a) { + if ($[0] !== a?.b?.c?.d?.e) { t1 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a; + $[0] = a?.b?.c?.d?.e; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md index 3b21db2328ab..9d95aec322f9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md @@ -44,7 +44,7 @@ import { useState, useEffect } from "react"; import { invoke, Stringify } from "shared-runtime"; function Content() { - const $ = _c(7); + const $ = _c(8); const [announcement, setAnnouncement] = useState(""); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { @@ -55,8 +55,7 @@ function Content() { } const [users, setUsers] = useState(t0); let t1; - let t2; - if ($[1] !== users) { + if ($[1] !== users?.length) { t1 = () => { if (users.length === 2) { let removedUserName = ""; @@ -66,26 +65,32 @@ function Content() { newUsers.pop(); return newUsers; }); + setAnnouncement(`Removed user (${removedUserName})`); } }; - t2 = [users]; - $[1] = users; + $[1] = users?.length; $[2] = t1; - $[3] = t2; } else { t1 = $[2]; - t2 = $[3]; + } + let t2; + if ($[3] !== users) { + t2 = [users]; + $[3] = users; + $[4] = t2; + } else { + t2 = $[4]; } useEffect(t1, t2); let t3; - if ($[4] !== announcement || $[5] !== users) { + if ($[5] !== announcement || $[6] !== users) { t3 = ; - $[4] = announcement; - $[5] = users; - $[6] = t3; + $[5] = announcement; + $[6] = users; + $[7] = t3; } else { - t3 = $[6]; + t3 = $[7]; } return t3; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md index 117031695888..1aeea2d8fdd2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-maybe-mutate-context-in-callback.expect.md @@ -41,11 +41,11 @@ function Component(props) { const $ = _c(5); const Foo = useContext(FooContext); let t0; - if ($[0] !== Foo) { + if ($[0] !== Foo?.current) { t0 = () => { mutate(Foo.current); }; - $[0] = Foo; + $[0] = Foo?.current; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md index 09dbbc230f3b..60d2c0aa67cd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback.expect.md @@ -34,11 +34,11 @@ function Component(props) { const $ = _c(5); const foo = useContext(FooContext); let t0; - if ($[0] !== foo) { + if ($[0] !== foo?.current) { t0 = () => { console.log(foo.current); }; - $[0] = foo; + $[0] = foo?.current; $[1] = t0; } else { t0 = $[1];