@samitouri / QOS-React-1 / commits / c60eebffea

[compiler] Add new ValidateNoVoidUseMemo pass (#33990)

Adds a new validation pass to validate against `useMemo`s that don't return anything. This usually indicates some kind of "useEffect"-like code that has side effects that need to be memoized to prevent overfiring, and is an anti-pattern. A follow up validation could also look at the return value of `useMemo`s to see if they are being used. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33990). * #34022 * #34002 * #34001 * __->__ #33990 * #33989

lauren committed Jul 28, 2025 at 12:46 UTC c60eebffea94a67f35c6ebbf7019e5b2145d4284
14 files changed +416
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+4
@@ -82,6 +82,7 @@ import {
82 import {inferTypes} from '../TypeInference';
83 import {
84 validateContextVariableLValues,
85 + validateNoVoidUseMemo,
86 validateHooksUsage,
87 validateMemoizedEffectDependencies,
88 validateNoCapitalizedCalls,
@@ -167,6 +168,9 @@ function runWithEnvironment(
168
169 validateContextVariableLValues(hir);
170 validateUseMemo(hir).unwrap();
171 + if (env.config.validateNoVoidUseMemo) {
172 + validateNoVoidUseMemo(hir).unwrap();
173 + }
174
175 if (
176 env.isInferredMemoEnabled &&
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+11
@@ -631,6 +631,17 @@ export const EnvironmentConfigSchema = z.object({
631 * ```
632 */
633 lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
634 +
635 + /**
636 + * If enabled, will validate useMemos that don't return any values:
637 + *
638 + * Valid:
639 + * useMemo(() => foo, [foo]);
640 + * useMemo(() => { return foo }, [foo]);
641 + * Invalid:
642 + * useMemo(() => { ... }, [...]);
643 + */
644 + validateNoVoidUseMemo: z.boolean().default(false),
645 });
646
647 export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoVoidUseMemo.ts new
+156
@@ -0,0 +1,156 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {CompilerError, ErrorSeverity} from '../CompilerError';
9 +import {
10 + HIRFunction,
11 + IdentifierId,
12 + FunctionExpression,
13 + SourceLocation,
14 + Environment,
15 + Instruction,
16 + getHookKindForType,
17 +} from '../HIR';
18 +import {Result} from '../Utils/Result';
19 +
20 +type TemporariesSidemap = {
21 + useMemoHooks: Map<IdentifierId, {name: string; loc: SourceLocation}>;
22 + funcExprs: Map<IdentifierId, FunctionExpression>;
23 + react: Set<IdentifierId>;
24 +};
25 +
26 +/**
27 + * Validates that useMemo has at least one explicit return statement.
28 + *
29 + * Valid cases:
30 + * - useMemo(() => value) // implicit arrow function return
31 + * - useMemo(() => { return value; }) // explicit return
32 + * - useMemo(() => { return; }) // explicit undefined
33 + * - useMemo(() => { if (cond) return val; }) // at least one return
34 + *
35 + * Invalid cases:
36 + * - useMemo(() => { console.log(); }) // no return statement at all
37 + */
38 +export function validateNoVoidUseMemo(
39 + fn: HIRFunction,
40 +): Result<void, CompilerError> {
41 + const errors = new CompilerError();
42 + const sidemap: TemporariesSidemap = {
43 + useMemoHooks: new Map(),
44 + funcExprs: new Map(),
45 + react: new Set(),
46 + };
47 +
48 + for (const [, block] of fn.body.blocks) {
49 + for (const instr of block.instructions) {
50 + collectTemporaries(instr, fn.env, sidemap);
51 + }
52 + }
53 +
54 + for (const [, block] of fn.body.blocks) {
55 + for (const instr of block.instructions) {
56 + if (instr.value.kind === 'CallExpression') {
57 + const callee = instr.value.callee.identifier;
58 + const useMemoHook = sidemap.useMemoHooks.get(callee.id);
59 +
60 + if (useMemoHook !== undefined && instr.value.args.length > 0) {
61 + const firstArg = instr.value.args[0];
62 + if (firstArg.kind !== 'Identifier') {
63 + continue;
64 + }
65 +
66 + let funcToCheck = sidemap.funcExprs.get(firstArg.identifier.id);
67 +
68 + if (!funcToCheck) {
69 + for (const [, searchBlock] of fn.body.blocks) {
70 + for (const searchInstr of searchBlock.instructions) {
71 + if (
72 + searchInstr.lvalue &&
73 + searchInstr.lvalue.identifier.id === firstArg.identifier.id &&
74 + searchInstr.value.kind === 'FunctionExpression'
75 + ) {
76 + funcToCheck = searchInstr.value;
77 + break;
78 + }
79 + }
80 + if (funcToCheck) break;
81 + }
82 + }
83 +
84 + if (funcToCheck) {
85 + const hasReturn = checkFunctionHasNonVoidReturn(
86 + funcToCheck.loweredFunc.func,
87 + );
88 +
89 + if (!hasReturn) {
90 + errors.push({
91 + severity: ErrorSeverity.InvalidReact,
92 + reason: `React Compiler has skipped optimizing this component because ${useMemoHook.name} doesn't return a value. ${useMemoHook.name} should only be used for memoizing values, not running arbitrary side effects.`,
93 + loc: useMemoHook.loc,
94 + suggestions: null,
95 + description: null,
96 + });
97 + }
98 + }
99 + }
100 + }
101 + }
102 + }
103 + return errors.asResult();
104 +}
105 +
106 +function checkFunctionHasNonVoidReturn(func: HIRFunction): boolean {
107 + for (const [, block] of func.body.blocks) {
108 + if (block.terminal.kind === 'return') {
109 + if (
110 + block.terminal.returnVariant === 'Explicit' ||
111 + block.terminal.returnVariant === 'Implicit'
112 + ) {
113 + return true;
114 + }
115 + }
116 + }
117 + return false;
118 +}
119 +
120 +function collectTemporaries(
121 + instr: Instruction,
122 + env: Environment,
123 + sidemap: TemporariesSidemap,
124 +): void {
125 + const {value, lvalue} = instr;
126 + switch (value.kind) {
127 + case 'FunctionExpression': {
128 + sidemap.funcExprs.set(lvalue.identifier.id, value);
129 + break;
130 + }
131 + case 'LoadGlobal': {
132 + const global = env.getGlobalDeclaration(value.binding, value.loc);
133 + const hookKind = global !== null ? getHookKindForType(env, global) : null;
134 + if (hookKind === 'useMemo') {
135 + sidemap.useMemoHooks.set(lvalue.identifier.id, {
136 + name: value.binding.name,
137 + loc: instr.loc,
138 + });
139 + } else if (value.binding.name === 'React') {
140 + sidemap.react.add(lvalue.identifier.id);
141 + }
142 + break;
143 + }
144 + case 'PropertyLoad': {
145 + if (sidemap.react.has(value.object.identifier.id)) {
146 + if (value.property === 'useMemo') {
147 + sidemap.useMemoHooks.set(lvalue.identifier.id, {
148 + name: value.property,
149 + loc: instr.loc,
150 + });
151 + }
152 + }
153 + break;
154 + }
155 + }
156 +}
compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts
+1
@@ -6,6 +6,7 @@
6 */
7
8 export {validateContextVariableLValues} from './ValidateContextVariableLValues';
9 +export {validateNoVoidUseMemo} from './ValidateNoVoidUseMemo';
10 export {validateHooksUsage} from './ValidateHooksUsage';
11 export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
12 export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useMemo-no-return-value.expect.md new
+41
@@ -0,0 +1,41 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoVoidUseMemo
6 +function Component() {
7 + const value = useMemo(() => {
8 + console.log('computing');
9 + }, []);
10 + const value2 = React.useMemo(() => {
11 + console.log('computing');
12 + }, []);
13 + return (
14 + <div>
15 + {value}
16 + {value2}
17 + </div>
18 + );
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 +Found 1 error:
28 +
29 +Error: React Compiler has skipped optimizing this component because useMemo doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.
30 +
31 +error.useMemo-no-return-value.ts:3:16
32 + 1 | // @validateNoVoidUseMemo
33 + 2 | function Component() {
34 +> 3 | const value = useMemo(() => {
35 + | ^^^^^^^ React Compiler has skipped optimizing this component because useMemo doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.
36 + 4 | console.log('computing');
37 + 5 | }, []);
38 + 6 | const value2 = React.useMemo(() => {
39 +```
40 +
41 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useMemo-no-return-value.js new
+15
@@ -0,0 +1,15 @@
1 +// @validateNoVoidUseMemo
2 +function Component() {
3 + const value = useMemo(() => {
4 + console.log('computing');
5 + }, []);
6 + const value2 = React.useMemo(() => {
7 + console.log('computing');
8 + }, []);
9 + return (
10 + <div>
11 + {value}
12 + {value2}
13 + </div>
14 + );
15 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoVoidUseMemo
6 +function Component() {
7 + const value = useMemo(() => computeValue(), []);
8 + return <div>{value}</div>;
9 +}
10 +
11 +```
12 +
13 +## Code
14 +
15 +```javascript
16 +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo
17 +function Component() {
18 + const $ = _c(2);
19 + let t0;
20 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
21 + t0 = computeValue();
22 + $[0] = t0;
23 + } else {
24 + t0 = $[0];
25 + }
26 + const value = t0;
27 + let t1;
28 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
29 + t1 = <div>{value}</div>;
30 + $[1] = t1;
31 + } else {
32 + t1 = $[1];
33 + }
34 + return t1;
35 +}
36 +
37 +```
38 +
39 +### Eval output
40 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.js new
+5
@@ -0,0 +1,5 @@
1 +// @validateNoVoidUseMemo
2 +function Component() {
3 + const value = useMemo(() => computeValue(), []);
4 + return <div>{value}</div>;
5 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.expect.md new
+34
@@ -0,0 +1,34 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoVoidUseMemo
6 +function Component() {
7 + const value = useMemo(() => {
8 + return;
9 + }, []);
10 + return <div>{value}</div>;
11 +}
12 +
13 +```
14 +
15 +## Code
16 +
17 +```javascript
18 +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo
19 +function Component() {
20 + const $ = _c(1);
21 + let t0;
22 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
23 + t0 = <div>{undefined}</div>;
24 + $[0] = t0;
25 + } else {
26 + t0 = $[0];
27 + }
28 + return t0;
29 +}
30 +
31 +```
32 +
33 +### Eval output
34 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.js new
+7
@@ -0,0 +1,7 @@
1 +// @validateNoVoidUseMemo
2 +function Component() {
3 + const value = useMemo(() => {
4 + return;
5 + }, []);
6 + return <div>{value}</div>;
7 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.expect.md new
+34
@@ -0,0 +1,34 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoVoidUseMemo
6 +function Component() {
7 + const value = useMemo(() => {
8 + return null;
9 + }, []);
10 + return <div>{value}</div>;
11 +}
12 +
13 +```
14 +
15 +## Code
16 +
17 +```javascript
18 +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo
19 +function Component() {
20 + const $ = _c(1);
21 + let t0;
22 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
23 + t0 = <div>{null}</div>;
24 + $[0] = t0;
25 + } else {
26 + t0 = $[0];
27 + }
28 + return t0;
29 +}
30 +
31 +```
32 +
33 +### Eval output
34 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.js new
+7
@@ -0,0 +1,7 @@
1 +// @validateNoVoidUseMemo
2 +function Component() {
3 + const value = useMemo(() => {
4 + return null;
5 + }, []);
6 + return <div>{value}</div>;
7 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.expect.md new
+51
@@ -0,0 +1,51 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoVoidUseMemo
6 +function Component({items}) {
7 + const value = useMemo(() => {
8 + for (let item of items) {
9 + if (item.match) return item;
10 + }
11 + return null;
12 + }, [items]);
13 + return <div>{value}</div>;
14 +}
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo
22 +function Component(t0) {
23 + const $ = _c(2);
24 + const { items } = t0;
25 + let t1;
26 + bb0: {
27 + for (const item of items) {
28 + if (item.match) {
29 + t1 = item;
30 + break bb0;
31 + }
32 + }
33 +
34 + t1 = null;
35 + }
36 + const value = t1;
37 + let t2;
38 + if ($[0] !== value) {
39 + t2 = <div>{value}</div>;
40 + $[0] = value;
41 + $[1] = t2;
42 + } else {
43 + t2 = $[1];
44 + }
45 + return t2;
46 +}
47 +
48 +```
49 +
50 +### Eval output
51 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.js new
+10
@@ -0,0 +1,10 @@
1 +// @validateNoVoidUseMemo
2 +function Component({items}) {
3 + const value = useMemo(() => {
4 + for (let item of items) {
5 + if (item.match) return item;
6 + }
7 + return null;
8 + }, [items]);
9 + return <div>{value}</div>;
10 +}