Skip to content

Commit 9eb6762

Browse files
committed
feat(compiler): improve preserved manual memoization diagnostic\n\nAdd CHANGELOG entry under Unreleased for enhanced forward reference guidance in babel-plugin-react-compiler.
1 parent 38bdda1 commit 9eb6762

File tree

7 files changed

+227
-3
lines changed

7 files changed

+227
-3
lines changed

compiler/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
## babel-plugin-react-compiler
4+
5+
* Improve diagnostic for preserved manual memoization when a callback references a later-declared memoized value; the message now includes the dependency name (when available), clarifies declaration order, and suggests moving the declaration earlier.
6+
17
## 19.1.0-rc.2 (May 14, 2025)
28

39
## babel-plugin-react-compiler

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,18 +540,25 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
540540
!this.scopes.has(identifier.scope.id) &&
541541
!this.prunedScopes.has(identifier.scope.id)
542542
) {
543+
const depName =
544+
identifier.name?.kind === 'named' ? identifier.name.value : null;
543545
state.errors.pushDiagnostic(
544546
CompilerDiagnostic.create({
545547
category: ErrorCategory.PreserveManualMemo,
546548
reason: 'Existing memoization could not be preserved',
547549
description: [
548550
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
549551
'This dependency may be mutated later, which could cause the value to change unexpectedly',
552+
depName
553+
? `. If '${depName}' is defined later in the component (e.g., via useMemo or useCallback), try moving this memoization after the dependency's declaration`
554+
: '',
550555
].join(''),
551556
}).withDetails({
552557
kind: 'error',
553558
loc,
554-
message: 'This dependency may be modified later',
559+
message: depName
560+
? `This dependency may be modified later. If '${depName}' is memoized, ensure it's declared before this hook`
561+
: 'This dependency may be modified later',
555562
}),
556563
);
557564
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-overlap-scopes.expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ Found 1 error:
4444
4545
Compilation Skipped: Existing memoization could not be preserved
4646
47-
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
47+
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly. If 'x' is defined later in the component (e.g., via useMemo or useCallback), try moving this memoization after the dependency's declaration.
4848
4949
error.false-positive-useMemo-overlap-scopes.ts:23:9
5050
21 | const result = useMemo(() => {
5151
22 | return [Math.max(x[1], a)];
5252
> 23 | }, [a, x]);
53-
| ^ This dependency may be modified later
53+
| ^ This dependency may be modified later. If 'x' is memoized, ensure it's declared before this hook
5454
24 | arrayPush(y, 3);
5555
25 | return {result, y};
5656
26 | }
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees:true
6+
import {useCallback, useMemo} from 'react';
7+
8+
/**
9+
* Issue: When a useCallback references a value from a useMemo that is
10+
* declared later in the component, the compiler triggers a false positive
11+
* preserve-manual-memoization error.
12+
*
13+
* The error occurs because the validation checks that dependencies have
14+
* completed their scope before the manual memo block starts. However,
15+
* when the callback is declared before the useMemo, the useMemo's scope
16+
* hasn't completed yet.
17+
*
18+
* This is a valid pattern in React - declaration order doesn't matter
19+
* for the runtime behavior since both are memoized.
20+
*/
21+
function Component({value}) {
22+
// This callback references `memoizedValue` which is declared later
23+
const callback = useCallback(() => {
24+
return memoizedValue + 1;
25+
}, [memoizedValue]);
26+
27+
// This useMemo is declared after the callback that uses it
28+
const memoizedValue = useMemo(() => {
29+
return value * 2;
30+
}, [value]);
31+
32+
return {callback, memoizedValue};
33+
}
34+
35+
export const FIXTURE_ENTRYPOINT = {
36+
fn: Component,
37+
params: [{value: 5}],
38+
};
39+
40+
```
41+
42+
43+
## Error
44+
45+
```
46+
Found 1 error:
47+
48+
Error: Cannot access variable before it is declared
49+
50+
`memoizedValue` is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.
51+
52+
error.useCallback-references-later-useMemo.ts:21:6
53+
19 | const callback = useCallback(() => {
54+
20 | return memoizedValue + 1;
55+
> 21 | }, [memoizedValue]);
56+
| ^^^^^^^^^^^^^ `memoizedValue` accessed before it is declared
57+
22 |
58+
23 | // This useMemo is declared after the callback that uses it
59+
24 | const memoizedValue = useMemo(() => {
60+
61+
error.useCallback-references-later-useMemo.ts:24:2
62+
22 |
63+
23 | // This useMemo is declared after the callback that uses it
64+
> 24 | const memoizedValue = useMemo(() => {
65+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66+
> 25 | return value * 2;
67+
| ^^^^^^^^^^^^^^^^^^^^^
68+
> 26 | }, [value]);
69+
| ^^^^^^^^^^^^^^^ `memoizedValue` is declared here
70+
27 |
71+
28 | return {callback, memoizedValue};
72+
29 | }
73+
```
74+
75+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @validatePreserveExistingMemoizationGuarantees:true
2+
import {useCallback, useMemo} from 'react';
3+
4+
/**
5+
* Issue: When a useCallback references a value from a useMemo that is
6+
* declared later in the component, the compiler triggers a false positive
7+
* preserve-manual-memoization error.
8+
*
9+
* The error occurs because the validation checks that dependencies have
10+
* completed their scope before the manual memo block starts. However,
11+
* when the callback is declared before the useMemo, the useMemo's scope
12+
* hasn't completed yet.
13+
*
14+
* This is a valid pattern in React - declaration order doesn't matter
15+
* for the runtime behavior since both are memoized.
16+
*/
17+
function Component({value}) {
18+
// This callback references `memoizedValue` which is declared later
19+
const callback = useCallback(() => {
20+
return memoizedValue + 1;
21+
}, [memoizedValue]);
22+
23+
// This useMemo is declared after the callback that uses it
24+
const memoizedValue = useMemo(() => {
25+
return value * 2;
26+
}, [value]);
27+
28+
return {callback, memoizedValue};
29+
}
30+
31+
export const FIXTURE_ENTRYPOINT = {
32+
fn: Component,
33+
params: [{value: 5}],
34+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees:true
6+
import {useCallback, useMemo} from 'react';
7+
8+
/**
9+
* This is the corrected version where the useMemo is declared before
10+
* the useCallback that references it. This should compile without errors.
11+
*/
12+
function Component({value}) {
13+
// useMemo declared first
14+
const memoizedValue = useMemo(() => {
15+
return value * 2;
16+
}, [value]);
17+
18+
// useCallback references the memoizedValue declared above
19+
const callback = useCallback(() => {
20+
return memoizedValue + 1;
21+
}, [memoizedValue]);
22+
23+
return {callback, memoizedValue};
24+
}
25+
26+
export const FIXTURE_ENTRYPOINT = {
27+
fn: Component,
28+
params: [{value: 5}],
29+
};
30+
31+
```
32+
33+
## Code
34+
35+
```javascript
36+
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees:true
37+
import { useCallback, useMemo } from "react";
38+
39+
/**
40+
* This is the corrected version where the useMemo is declared before
41+
* the useCallback that references it. This should compile without errors.
42+
*/
43+
function Component(t0) {
44+
const $ = _c(5);
45+
const { value } = t0;
46+
47+
const memoizedValue = value * 2;
48+
let t1;
49+
if ($[0] !== memoizedValue) {
50+
t1 = () => memoizedValue + 1;
51+
$[0] = memoizedValue;
52+
$[1] = t1;
53+
} else {
54+
t1 = $[1];
55+
}
56+
const callback = t1;
57+
let t2;
58+
if ($[2] !== callback || $[3] !== memoizedValue) {
59+
t2 = { callback, memoizedValue };
60+
$[2] = callback;
61+
$[3] = memoizedValue;
62+
$[4] = t2;
63+
} else {
64+
t2 = $[4];
65+
}
66+
return t2;
67+
}
68+
69+
export const FIXTURE_ENTRYPOINT = {
70+
fn: Component,
71+
params: [{ value: 5 }],
72+
};
73+
74+
```
75+
76+
### Eval output
77+
(kind: ok) {"callback":"[[ function params=0 ]]","memoizedValue":10}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @validatePreserveExistingMemoizationGuarantees:true
2+
import {useCallback, useMemo} from 'react';
3+
4+
/**
5+
* This is the corrected version where the useMemo is declared before
6+
* the useCallback that references it. This should compile without errors.
7+
*/
8+
function Component({value}) {
9+
// useMemo declared first
10+
const memoizedValue = useMemo(() => {
11+
return value * 2;
12+
}, [value]);
13+
14+
// useCallback references the memoizedValue declared above
15+
const callback = useCallback(() => {
16+
return memoizedValue + 1;
17+
}, [memoizedValue]);
18+
19+
return {callback, memoizedValue};
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: Component,
24+
params: [{value: 5}],
25+
};

0 commit comments

Comments
 (0)