Skip to content

Conversation

@kuomars110
Copy link

Summary
This PR fixes a critical bug in the React Compiler where functions with closure-scoped variables were incorrectly being outlined (hoisted) to top-level, breaking lexical scope and causing runtime errors.

Problem: The compiler's outlineFunctions optimization was only checking if context.length === 0 before outlining a function. However, this check only validates the immediate component scope, not parent scopes. When a nested component creates a closure over its parent component's variables (e.g., () => store), those variables aren't tracked in the nested component's [context] array, but the function was still being incorrectly outlined.

Root Cause: In the reported issue #34901, the arrow function () => store captures [store] from the parent scope. When the compiler outlined this to a top-level _temp() function, [store] became undefined, causing a runtime error.

Solution: Added a [hasUnaccountedOuterScopeReferences()] helper function that recursively checks for LoadContext HIR instructions. These instructions indicate that a function is accessing variables from outer scopes that aren't tracked in the [context] array. Functions with such references are now prevented from being outlined.

Reference: Fixes #34901

How did you test this change?
Added a regression test case ([closure-hoisting-bug.js] that reproduces the exact scenario from issue #34901:
A parent function creates a local variable [store]
A nested component uses that variable in a closure (() => store)
Verified the compiled output keeps the arrow function inline instead of incorrectly outlining it

Ran the compiler build and tests:
cd compiler yarn build yarn test --testPathPattern="fixtures" --testNamePattern="closure-hoisting-bug"

Verified the fix:
The test case now compiles correctly with () => store staying inline within the component
The compiled code properly accesses [store] from the closure
Eval output shows the expected result:

hello

Code formatting and linting:
npx prettier --write OutlineFunctions.ts closure-hoisting-bug.js

All files pass lint checks with no errors.

The fix ensures that the compiler's optimization respects lexical scope and doesn't break closure semantics, preventing the undefined variable runtime errors reported in #34901.

…ebook#34901)

The React Compiler incorrectly outlines closure-scoped functions to the
top level when it detects they have no context variables from the
immediate component scope. This breaks lexical scoping for nested
components that capture variables from parent scopes.

The issue occurs when:
1. A component contains a nested function component
2. That nested component has an arrow function
3. The arrow function captures a variable from the parent component

The `outlineFunctions` optimization only checked `context.length === 0`,
which verifies variables from the immediate component scope, but doesn't
account for variables from outer parent scopes accessed via LoadContext
instructions.

Fix: Added `hasUnaccountedOuterScopeReferences()` helper that checks for
LoadContext instructions in the function's HIR. If any exist, the
function cannot be safely outlined as it depends on runtime context.

Test: Added closure-hoisting-bug.js fixture that reproduces the issue
and verifies the fix.
@meta-cla
Copy link

meta-cla bot commented Oct 19, 2025

Hi @kuomars110!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla meta-cla bot added the CLA Signed label Oct 19, 2025
@meta-cla
Copy link

meta-cla bot commented Oct 19, 2025

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

Copy link
Member

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! See comments, but overall this looks good

…tion naming

[Version] Update React version to 19.3.0
@kuomars110
Copy link
Author

Thanks for the review! I went through everything you mentioned please take another look when you can.

Copy link
Member

@josephsavona josephsavona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked this locally and this is the wrong fix. We already check for context variables — and in general we try to add outlined functions as siblings of the original component in order to have access to the same variables. This just isn't working for factory functions where the inner component is defined with an arrow function.

In your example, change Component to a regular function expression (not an arrow function) and it will already work.

@kuomars110
Copy link
Author

Thank you for clarifying! You're absolutely right. I misunderstood the root cause.

Looking at the original issue #34901 again, the problem is specifically with arrow functions inside factory functions like:

`export function createSomething() {
const store = new SomeStore();

const Cmp = () => { // Arrow function component
const observedStore = useLocalObservable(() => store); // This arrow captures store
return

{observedStore.test}
;
};

return Cmp;
}`

The issue is that:

Cmp (the arrow function) has [context.length === 0]
But the inner arrow () => store is being outlined when it shouldn't be
The inner arrow function is being evaluated in the context of Cmp where [store] accessible via closure from the parent createSomething
Should the fix instead be checking whether the parent component itself is an outlined function or checking the nesting level? Or is this a case where we shouldn't outline functions that are inside components returned from factory functions?

Could you point me in the right direction for the correct approach?

@josephsavona
Copy link
Member

There are a few different directions to go, and the implications are subtle enough that I think this is something that we'll have to look into and fix ourselves. I appreciate the interest!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compiler Bug]: Incorrect closure hoisting causes runtime error

2 participants