Skip to content

Conversation

@poteto
Copy link
Member

@poteto poteto commented Aug 26, 2025

Previously, the compiler would incorrectly attempt to compile nested components/hooks defined inside non-React functions. This would lead to scope reference errors at runtime because the compiler would optimize the nested React function without understanding its closure over the parent function's variables.

This PR adds detection when non-React functions declare components or hooks, and reports a clear error before compilation. I put this under a new compiler flag defaulting to false. I'll run a test on this internally first, but I expect we should be able to just turn it on in both compiler (so we stop miscompiling) and linter.

Closes #33978

Playground example: https://react-compiler-playground-git-pr34305-fbopensource.vercel.app/#N4Igzg9grgTgxgUxALhAejQAgAIDcCGANgJYAm+ALggHIQAiAngHb4C2xcRhDAwjApQSkeEVgAcITBEwpgA8jAASECAGswAHSkAPCTAqYAZlCZwKxSZgDmCCgEkmYqBQAU+AJSZgWzJjiSwAwB1GHwxMQQYTABeTBdPaIA+Lx9fPwCDAAt8JlJCBB5sphsYuITk7yY0tPwAOklCnJt4gG5U3wBfNqZ2zH4KWCqAHmJHZ0wGopto4CK8gqmEDsw0RO7O7tT+wcwQsIiYbo6QDqA

@meta-cla meta-cla bot added the CLA Signed label Aug 26, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Aug 26, 2025
@poteto poteto force-pushed the pr34305 branch 3 times, most recently from 991872c to e940573 Compare August 26, 2025 19:12
@poteto poteto requested review from josephsavona and mofeiZ August 26, 2025 19:13
Copy link
Contributor

@jorge-cab jorge-cab left a comment

Choose a reason for hiding this comment

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

A more general question. When does Program.ts run? does it run in-between compiler passes?

return;
}

if (programContext.alreadyCompiled.has(fn.node)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How does alreadyCompiled works? I'm guessing it keep a cache of all compiled functions to avoid repeating work?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it's just a simple Set containing all the functions we had already compiled. We added this a long time ago when we saw functions getting compiled multiple times.

// In 'all' mode, compile only top level functions
if (
pass.opts.compilationMode === 'all' &&
fn.scope.getProgramParent() !== fn.scope.parent
Copy link
Contributor

Choose a reason for hiding this comment

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

What would fn.scope.getProgramParent === fn.scope.parent mean vs. what you wrote in this line?

Copy link
Member Author

Choose a reason for hiding this comment

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

scope.getProgramParent() is a Babel API that returns the Program node. Comparing these 2 nodes just checks if the fn is in module scope (if it was in module scope, it's parent would also be the Program), or if it's nested within another scope (such as in a BlockStatement). These ASTexplorer console logs might help visualize that better

if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {

if (pass.opts.environment?.validateNoComponentOrHookFactories) {
if (fnType === null || fnType === 'Other') {
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes it so we don't try to validate top level React Components/Hooks right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, for now this validation just checks to see if the parent of a component/hook is a non-React function.

nestedFn.skip();
};

fn.traverse({
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly fn: BabelFn is an AST node here and you just traverse recursively to check if you have an n deep React Component/Hook and parentName is always the direct parent of the nested function, is this accurate?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah this would traverse any nested Functions and just check each parent/child pair to see if the parent and child are both React functions

@poteto
Copy link
Member Author

poteto commented Aug 27, 2025

A more general question. When does Program.ts run? does it run in-between compiler passes?

It runs right at the start of compilation, before we lower the AST into HIR, and after we parse the compiler options. The babel plugin calls Program:compileProgram which then kicks off the core compiler passes.

I needed to make this fix at the AST level, since we were already skipping over non-React functions there (and thus wouldn't have been available within the core compiler pipeline, since it wouldn't have been a part of the HIR).

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.

The validation logic looks good, but i think we should be more clear on how we frame the error to the user

@poteto poteto force-pushed the pr34305 branch 3 times, most recently from f4a7f65 to 8990ebf Compare August 27, 2025 16:56
{
kind: 'error',
message: 'the component is created here',
loc: nestedFn.node.loc ?? null,
Copy link
Member

Choose a reason for hiding this comment

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

I wonder about trying to target the location of the function name, and falling back to the full function scope only if there was no name. Unfortunately Babel doesn't provide a location for the function keyword that we could target.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking about that too. I think we can use the identifier if its present or fallback to the entire function if there's nothing available to use.

Copy link
Member Author

Choose a reason for hiding this comment

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

updated!

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.

awesome

Previously, the compiler would incorrectly attempt to compile nested components/hooks defined inside non-React functions. This would lead to scope reference errors at runtime because the compiler would optimize the nested React function without understanding its closure over the parent function's variables.

This PR adds detection when non-React functions declare components or hooks, and reports a clear error before compilation. I put this under a new compiler flag defaulting to false. I'll run a test on this internally first, but I expect we should be able to just turn it on in both compiler (so we stop miscompiling) and linter.

Closes #33978

Playground example: https://react-compiler-playground-git-pr34305-fbopensource.vercel.app/#N4Igzg9grgTgxgUxALhAejQAgAIDcCGANgJYAm+ALggHIQAiAngHb4C2xcRhDAwjApQSkeEVgAcITBEwpgA8jAASECAGswAHSkAPCTAqYAZlCZwKxSZgDmCCgEkmYqBQAU+AJSZgWzJjiSwAwB1GHwxMQQYTABeTBdPaIA+Lx9fPwCDAAt8JlJCBB5sphsYuITk7yY0tPwAOklCnJt4gG5U3wBfNqZ2zH4KWCqAHmJHZ0wGopto4CK8gqmEDsw0RO7O7tT+wcwQsIiYbo6QDqA
@poteto poteto merged commit b870042 into main Aug 27, 2025
21 checks passed
@poteto poteto deleted the pr34305 branch August 27, 2025 17:59
github-actions bot pushed a commit that referenced this pull request Aug 27, 2025
Previously, the compiler would incorrectly attempt to compile nested
components/hooks defined inside non-React functions. This would lead to
scope reference errors at runtime because the compiler would optimize
the nested React function without understanding its closure over the
parent function's variables.

This PR adds detection when non-React functions declare components or
hooks, and reports a clear error before compilation. I put this under a
new compiler flag defaulting to false. I'll run a test on this
internally first, but I expect we should be able to just turn it on in
both compiler (so we stop miscompiling) and linter.

Closes #33978

Playground example:
https://react-compiler-playground-git-pr34305-fbopensource.vercel.app/#N4Igzg9grgTgxgUxALhAejQAgAIDcCGANgJYAm+ALggHIQAiAngHb4C2xcRhDAwjApQSkeEVgAcITBEwpgA8jAASECAGswAHSkAPCTAqYAZlCZwKxSZgDmCCgEkmYqBQAU+AJSZgWzJjiSwAwB1GHwxMQQYTABeTBdPaIA+Lx9fPwCDAAt8JlJCBB5sphsYuITk7yY0tPwAOklCnJt4gG5U3wBfNqZ2zH4KWCqAHmJHZ0wGopto4CK8gqmEDsw0RO7O7tT+wcwQsIiYbo6QDqA

DiffTrain build for [b870042](b870042)
github-actions bot pushed a commit that referenced this pull request Aug 27, 2025
Previously, the compiler would incorrectly attempt to compile nested
components/hooks defined inside non-React functions. This would lead to
scope reference errors at runtime because the compiler would optimize
the nested React function without understanding its closure over the
parent function's variables.

This PR adds detection when non-React functions declare components or
hooks, and reports a clear error before compilation. I put this under a
new compiler flag defaulting to false. I'll run a test on this
internally first, but I expect we should be able to just turn it on in
both compiler (so we stop miscompiling) and linter.

Closes #33978

Playground example:
https://react-compiler-playground-git-pr34305-fbopensource.vercel.app/#N4Igzg9grgTgxgUxALhAejQAgAIDcCGANgJYAm+ALggHIQAiAngHb4C2xcRhDAwjApQSkeEVgAcITBEwpgA8jAASECAGswAHSkAPCTAqYAZlCZwKxSZgDmCCgEkmYqBQAU+AJSZgWzJjiSwAwB1GHwxMQQYTABeTBdPaIA+Lx9fPwCDAAt8JlJCBB5sphsYuITk7yY0tPwAOklCnJt4gG5U3wBfNqZ2zH4KWCqAHmJHZ0wGopto4CK8gqmEDsw0RO7O7tT+wcwQsIiYbo6QDqA

DiffTrain build for [b870042](b870042)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compiler Bug]: Component defined inside function cause incorrect optimization

4 participants