[compiler] Always error on async reassignments
Summary: Addresses the issue in #30109: any mutation of a local in an async function may occur after rendering has finished. ghstack-source-id: 9f15cf0f144c0badd6009ceb51df43a50399d82b Pull Request resolved: https://github.com/facebook/react/pull/30111
Mike Vitousek committed
Jun 26, 2024 at 17:07 UTC
fcfbfc1d1e3e350e1ea3fbc4a2418f647dc28a31
5 files changed
+59
-67
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
+22
-3
@@ -19,7 +19,12 @@ import {
19
*/
20
export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
21
const contextVariables = new Set<IdentifierId>();
22
- const reassignment = getContextReassignment(fn, contextVariables, false);
22
+ const reassignment = getContextReassignment(
23
+ fn,
24
+ contextVariables,
25
+ false,
26
+ false
27
+ );
28
if (reassignment !== null) {
29
CompilerError.throwInvalidReact({
30
reason:
@@ -37,7 +42,8 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
42
function getContextReassignment(
43
fn: HIRFunction,
44
contextVariables: Set<IdentifierId>,
40
- isFunctionExpression: boolean
45
+ isFunctionExpression: boolean,
46
+ isAsync: boolean
47
): Place | null {
48
const reassigningFunctions = new Map<IdentifierId, Place>();
49
for (const [, block] of fn.body.blocks) {
@@ -49,7 +55,8 @@ function getContextReassignment(
55
let reassignment = getContextReassignment(
56
value.loweredFunc.func,
57
contextVariables,
52
- true
58
+ true,
59
+ isAsync || value.loweredFunc.func.async
60
);
61
if (reassignment === null) {
62
// If the function itself doesn't reassign, does one of its dependencies?
@@ -65,6 +72,18 @@ function getContextReassignment(
72
}
73
// if the function or its depends reassign, propagate that fact on the lvalue
74
if (reassignment !== null) {
75
+ if (isAsync || value.loweredFunc.func.async) {
76
+ CompilerError.throwInvalidReact({
77
+ reason:
78
+ "Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead",
79
+ description:
80
+ reassignment.identifier.name !== null &&
81
+ reassignment.identifier.name.kind === "named"
82
+ ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
83
+ : "",
84
+ loc: reassignment.loc,
85
+ });
86
+ }
87
reassigningFunctions.set(lvalue.identifier.id, reassignment);
88
}
89
break;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.expect.md
new
+37
@@ -0,0 +1,37 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component() {
6
+ let value = null;
7
+ const reassign = async () => {
8
+ await foo().then((result) => {
9
+ // Reassigning a local variable in an async function is *always* mutating
10
+ // after render, so this should error regardless of where this ends up
11
+ // getting called
12
+ value = result;
13
+ });
14
+ };
15
+
16
+ const onClick = async () => {
17
+ await reassign();
18
+ };
19
+ return <div onClick={onClick}>Click</div>;
20
+}
21
+
22
+```
23
+
24
+
25
+## Error
26
+
27
+```
28
+ 6 | // after render, so this should error regardless of where this ends up
29
+ 7 | // getting called
30
+> 8 | value = result;
31
+ | ^^^^^ InvalidReact: Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `value` cannot be reassigned after render (8:8)
32
+ 9 | });
33
+ 10 | };
34
+ 11 |
35
+```
36
+
37
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.js
renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
deleted
-58
@@ -1,58 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function Component() {
6
- let value = null;
7
- const reassign = async () => {
8
- await foo().then((result) => {
9
- // Reassigning a local variable in an async function is *always* mutating
10
- // after render, so this should error regardless of where this ends up
11
- // getting called
12
- value = result;
13
- });
14
- };
15
-
16
- const onClick = async () => {
17
- await reassign();
18
- };
19
- return <div onClick={onClick}>Click</div>;
20
-}
21
-
22
-```
23
-
24
-## Code
25
-
26
-```javascript
27
-import { c as _c } from "react/compiler-runtime";
28
-function Component() {
29
- const $ = _c(2);
30
- let value;
31
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
32
- value = null;
33
- $[0] = value;
34
- } else {
35
- value = $[0];
36
- }
37
- let t0;
38
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
39
- const reassign = async () => {
40
- await foo().then((result) => {
41
- value = result;
42
- });
43
- };
44
-
45
- const onClick = async () => {
46
- await reassign();
47
- };
48
-
49
- t0 = <div onClick={onClick}>Click</div>;
50
- $[1] = t0;
51
- } else {
52
- t0 = $[1];
53
- }
54
- return t0;
55
-}
56
-
57
-```
58
-
\ No newline at end of file
compiler/packages/snap/src/SproutTodoFilter.ts
-6
@@ -483,12 +483,6 @@ const skipFilter = new Set([
483
"rules-of-hooks/rules-of-hooks-93dc5d5e538a",
484
"rules-of-hooks/rules-of-hooks-69521d94fa03",
485
486
- // should error
487
- "todo.invalid-reassign-local-variable-in-jsx-callback",
488
- "todo.invalid-reassign-local-variable-in-hook-argument",
489
- "todo.invalid-reassign-local-variable-in-effect",
490
- "todo.invalid-reassign-local-variable-in-async-callback",
491
-
486
// bugs
487
"bug-invalid-hoisting-functionexpr",
488
"original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",