@samitouri / QOS-React-2 / commits / b5c1637109

[compiler] Reuse DropManualMemoization for ValidateNoVoidUseMemo (#34001)

Much of the logic in the new validation pass is already implemented in DropManualMemoization, so let's combine them. I opted to keep the environment flag so we can more precisely control the rollout. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34001). * #34022 * #34002 * __->__ #34001

lauren committed Jul 28, 2025 at 12:54 UTC b5c16371091c1abbf77ec944c755bd139abc7568
5 files changed +94 -169
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+1 -5
@@ -82,7 +82,6 @@ import {
82 import {inferTypes} from '../TypeInference';
83 import {
84 validateContextVariableLValues,
85 - validateNoVoidUseMemo,
85 validateHooksUsage,
86 validateMemoizedEffectDependencies,
87 validateNoCapitalizedCalls,
@@ -168,9 +167,6 @@ function runWithEnvironment(
167
168 validateContextVariableLValues(hir);
169 validateUseMemo(hir).unwrap();
171 - if (env.config.validateNoVoidUseMemo) {
172 - validateNoVoidUseMemo(hir).unwrap();
173 - }
170
171 if (
172 env.isInferredMemoEnabled &&
@@ -178,7 +174,7 @@ function runWithEnvironment(
174 !env.config.disableMemoizationForDebugging &&
175 !env.config.enableChangeDetectionForDebugging
176 ) {
181 - dropManualMemoization(hir);
177 + dropManualMemoization(hir).unwrap();
178 log({kind: 'hir', name: 'DropManualMemoization', value: hir});
179 }
180
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+65 -2
@@ -5,7 +5,12 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {CompilerError, SourceLocation} from '..';
8 +import {
9 + CompilerDiagnostic,
10 + CompilerError,
11 + ErrorSeverity,
12 + SourceLocation,
13 +} from '..';
14 import {
15 CallExpression,
16 Effect,
@@ -30,6 +35,7 @@ import {
35 makeInstructionId,
36 } from '../HIR';
37 import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
38 +import {Result} from '../Utils/Result';
39
40 type ManualMemoCallee = {
41 kind: 'useMemo' | 'useCallback';
@@ -341,8 +347,14 @@ function extractManualMemoizationArgs(
347 * rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking
348 * of globals and property loads to find both direct calls as well as usage via the React namespace,
349 * eg `React.useMemo()`.
350 + *
351 + * This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
352 + * is only used for memoizing values and not for running arbitrary side effects.
353 */
345 -export function dropManualMemoization(func: HIRFunction): void {
354 +export function dropManualMemoization(
355 + func: HIRFunction,
356 +): Result<void, CompilerError> {
357 + const errors = new CompilerError();
358 const isValidationEnabled =
359 func.env.config.validatePreserveExistingMemoizationGuarantees ||
360 func.env.config.validateNoSetStateInRender ||
@@ -390,6 +402,41 @@ export function dropManualMemoization(func: HIRFunction): void {
402 manualMemo.kind,
403 sidemap,
404 );
405 +
406 + /**
407 + * Bailout on void return useMemos. This is an anti-pattern where code might be using
408 + * useMemo like useEffect: running arbirtary side-effects synced to changes in specific
409 + * values.
410 + */
411 + if (
412 + func.env.config.validateNoVoidUseMemo &&
413 + manualMemo.kind === 'useMemo'
414 + ) {
415 + const funcToCheck = sidemap.functions.get(
416 + fnPlace.identifier.id,
417 + )?.value;
418 + if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
419 + if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
420 + errors.pushDiagnostic(
421 + CompilerDiagnostic.create({
422 + severity: ErrorSeverity.InvalidReact,
423 + category: 'useMemo() callbacks must return a value',
424 + description: `This ${
425 + manualMemo.loadInstr.value.kind === 'PropertyLoad'
426 + ? 'React.useMemo'
427 + : 'useMemo'
428 + } callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
429 + suggestions: null,
430 + }).withDetail({
431 + kind: 'error',
432 + loc: instr.value.loc,
433 + message: 'useMemo() callbacks must return a value',
434 + }),
435 + );
436 + }
437 + }
438 + }
439 +
440 instr.value = getManualMemoizationReplacement(
441 fnPlace,
442 instr.value.loc,
@@ -486,6 +533,8 @@ export function dropManualMemoization(func: HIRFunction): void {
533 markInstructionIds(func.body);
534 }
535 }
536 +
537 + return errors.asResult();
538 }
539
540 function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
@@ -530,3 +579,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
579 }
580 return optionals;
581 }
582 +
583 +function hasNonVoidReturn(func: HIRFunction): boolean {
584 + for (const [, block] of func.body.blocks) {
585 + if (block.terminal.kind === 'return') {
586 + if (
587 + block.terminal.returnVariant === 'Explicit' ||
588 + block.terminal.returnVariant === 'Implicit'
589 + ) {
590 + return true;
591 + }
592 + }
593 + }
594 + return false;
595 +}
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoVoidUseMemo.ts deleted
-156
@@ -1,156 +0,0 @@
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,7 +6,6 @@
6 */
7
8 export {validateContextVariableLValues} from './ValidateContextVariableLValues';
9 -export {validateNoVoidUseMemo} from './ValidateNoVoidUseMemo';
9 export {validateHooksUsage} from './ValidateHooksUsage';
10 export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
11 export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useMemo-no-return-value.expect.md
+28 -5
@@ -24,18 +24,41 @@ function Component() {
24 ## Error
25
26 ```
27 -Found 1 error:
27 +Found 2 errors:
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.
29 +Error: useMemo() callbacks must return a value
30 +
31 +This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
32
33 error.useMemo-no-return-value.ts:3:16
34 1 | // @validateNoVoidUseMemo
35 2 | function Component() {
36 > 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 | }, []);
37 + | ^^^^^^^^^^^^^^^
38 +> 4 | console.log('computing');
39 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40 +> 5 | }, []);
41 + | ^^^^^^^^^ useMemo() callbacks must return a value
42 6 | const value2 = React.useMemo(() => {
43 + 7 | console.log('computing');
44 + 8 | }, []);
45 +
46 +Error: useMemo() callbacks must return a value
47 +
48 +This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
49 +
50 +error.useMemo-no-return-value.ts:6:17
51 + 4 | console.log('computing');
52 + 5 | }, []);
53 +> 6 | const value2 = React.useMemo(() => {
54 + | ^^^^^^^^^^^^^^^^^^^^^
55 +> 7 | console.log('computing');
56 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57 +> 8 | }, []);
58 + | ^^^^^^^^^ useMemo() callbacks must return a value
59 + 9 | return (
60 + 10 | <div>
61 + 11 | {value}
62 ```
63
64
\ No newline at end of file