@samitouri / QOS-React / commits / 6b1a2c1d81

fix(react-compiler): optimize components declared with arrow function and implicit return and `compilationMode: 'infer'` (#31792)

fixes https://github.com/facebook/react/issues/31601 https://github.com/facebook/react/issues/31639 cc @josephsavona

Dimitri POSTOLOV committed Mar 22, 2025 at 00:46 UTC 6b1a2c1d81630a5f385c5be0f758365b63d92eae
3 files changed +71 -17
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+25 -17
@@ -1008,31 +1008,39 @@ function callsHooksOrCreatesJsx(
1008 return invokesHooks || createsJsx;
1009 }
1010
1011 +function isNonNode(node?: t.Expression | null): boolean {
1012 + if (!node) {
1013 + return true;
1014 + }
1015 + switch (node.type) {
1016 + case 'ObjectExpression':
1017 + case 'ArrowFunctionExpression':
1018 + case 'FunctionExpression':
1019 + case 'BigIntLiteral':
1020 + case 'ClassExpression':
1021 + case 'NewExpression': // technically `new Array()` is legit, but unlikely
1022 + return true;
1023 + }
1024 + return false;
1025 +}
1026 +
1027 function returnsNonNode(
1028 node: NodePath<
1029 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
1030 >,
1031 ): boolean {
1016 - let hasReturn = false;
1032 let returnsNonNode = false;
1033 + if (
1034 + // node.traverse#ArrowFunctionExpression isn't called for the root node
1035 + node.type === 'ArrowFunctionExpression' &&
1036 + node.node.body.type !== 'BlockStatement'
1037 + ) {
1038 + returnsNonNode = isNonNode(node.node.body);
1039 + }
1040
1041 node.traverse({
1042 ReturnStatement(ret) {
1021 - hasReturn = true;
1022 - const argument = ret.node.argument;
1023 - if (argument == null) {
1024 - returnsNonNode = true;
1025 - } else {
1026 - switch (argument.type) {
1027 - case 'ObjectExpression':
1028 - case 'ArrowFunctionExpression':
1029 - case 'FunctionExpression':
1030 - case 'BigIntLiteral':
1031 - case 'ClassExpression':
1032 - case 'NewExpression': // technically `new Array()` is legit, but unlikely
1033 - returnsNonNode = true;
1034 - }
1035 - }
1043 + returnsNonNode = isNonNode(ret.node.argument);
1044 },
1045 // Skip traversing all nested functions and their return statements
1046 ArrowFunctionExpression: skipNestedFunctions(node),
@@ -1041,7 +1049,7 @@ function returnsNonNode(
1049 ObjectMethod: node => node.skip(),
1050 });
1051
1044 - return !hasReturn || returnsNonNode;
1052 + return returnsNonNode;
1053 }
1054
1055 /*
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/arrow-function-with-implicit-return.expect.md new
+39
@@ -0,0 +1,39 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @compilationMode(infer)
6 +const Test = () => <div />;
7 +
8 +export const FIXTURE_ENTRYPOINT = {
9 + fn: Test,
10 + params: [{}],
11 +};
12 +
13 +```
14 +
15 +## Code
16 +
17 +```javascript
18 +import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
19 +const Test = () => {
20 + const $ = _c(1);
21 + let t0;
22 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
23 + t0 = <div />;
24 + $[0] = t0;
25 + } else {
26 + t0 = $[0];
27 + }
28 + return t0;
29 +};
30 +
31 +export const FIXTURE_ENTRYPOINT = {
32 + fn: Test,
33 + params: [{}],
34 +};
35 +
36 +```
37 +
38 +### Eval output
39 +(kind: ok) <div></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/arrow-function-with-implicit-return.js new
+7
@@ -0,0 +1,7 @@
1 +// @compilationMode(infer)
2 +const Test = () => <div />;
3 +
4 +export const FIXTURE_ENTRYPOINT = {
5 + fn: Test,
6 + params: [{}],
7 +};