[compiler][repro] Postfix operator is incorrectly compiled (#33508)
This bug was reported via our wg and appears to only affect values created as a ref. Currently, postfix operators used in a callback gets compiled to: ```js modalId.current = modalId.current + 1; // 1 const id = modalId.current; // 1 return id; ``` which is semantically incorrect. The postfix increment operator should return the value before incrementing. In other words something like this should have been compiled instead: ```js const id = modalId.current; // 0 modalId.current = modalId.current + 1; // 1 return id; ``` This bug does not trigger when the incremented value is a plain primitive, instead there is a TODO bailout.
lauren committed
Jun 11, 2025 at 14:40 UTC
888ea60d8e6059790382c0b849be13915b4bcc30
3 files changed
+175
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md
new
+132
@@ -0,0 +1,132 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {useRef, useEffect} from 'react';
6
+
7
+/**
8
+ * The postfix increment operator should return the value before incrementing.
9
+ * ```js
10
+ * const id = count.current; // 0
11
+ * count.current = count.current + 1; // 1
12
+ * return id;
13
+ * ```
14
+ * The bug is that we currently increment the value before the expression is evaluated.
15
+ * This bug does not trigger when the incremented value is a plain primitive.
16
+ *
17
+ * Found differences in evaluator results
18
+ * Non-forget (expected):
19
+ * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
20
+ * logs: ['id = 0','count = 1']
21
+ * Forget:
22
+ * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
23
+ * logs: ['id = 1','count = 1']
24
+ */
25
+function useFoo() {
26
+ const count = useRef(0);
27
+ const updateCountPostfix = () => {
28
+ const id = count.current++;
29
+ return id;
30
+ };
31
+ const updateCountPrefix = () => {
32
+ const id = ++count.current;
33
+ return id;
34
+ };
35
+ useEffect(() => {
36
+ const id = updateCountPostfix();
37
+ console.log(`id = ${id}`);
38
+ console.log(`count = ${count.current}`);
39
+ }, []);
40
+ return {count, updateCountPostfix, updateCountPrefix};
41
+}
42
+
43
+export const FIXTURE_ENTRYPOINT = {
44
+ fn: useFoo,
45
+ params: [],
46
+};
47
+
48
+```
49
+
50
+## Code
51
+
52
+```javascript
53
+import { c as _c } from "react/compiler-runtime";
54
+import { useRef, useEffect } from "react";
55
+
56
+/**
57
+ * The postfix increment operator should return the value before incrementing.
58
+ * ```js
59
+ * const id = count.current; // 0
60
+ * count.current = count.current + 1; // 1
61
+ * return id;
62
+ * ```
63
+ * The bug is that we currently increment the value before the expression is evaluated.
64
+ * This bug does not trigger when the incremented value is a plain primitive.
65
+ *
66
+ * Found differences in evaluator results
67
+ * Non-forget (expected):
68
+ * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
69
+ * logs: ['id = 0','count = 1']
70
+ * Forget:
71
+ * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
72
+ * logs: ['id = 1','count = 1']
73
+ */
74
+function useFoo() {
75
+ const $ = _c(5);
76
+ const count = useRef(0);
77
+ let t0;
78
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
79
+ t0 = () => {
80
+ count.current = count.current + 1;
81
+ const id = count.current;
82
+ return id;
83
+ };
84
+ $[0] = t0;
85
+ } else {
86
+ t0 = $[0];
87
+ }
88
+ const updateCountPostfix = t0;
89
+ let t1;
90
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
91
+ t1 = () => {
92
+ const id_0 = (count.current = count.current + 1);
93
+ return id_0;
94
+ };
95
+ $[1] = t1;
96
+ } else {
97
+ t1 = $[1];
98
+ }
99
+ const updateCountPrefix = t1;
100
+ let t2;
101
+ let t3;
102
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
103
+ t2 = () => {
104
+ const id_1 = updateCountPostfix();
105
+ console.log(`id = ${id_1}`);
106
+ console.log(`count = ${count.current}`);
107
+ };
108
+ t3 = [];
109
+ $[2] = t2;
110
+ $[3] = t3;
111
+ } else {
112
+ t2 = $[2];
113
+ t3 = $[3];
114
+ }
115
+ useEffect(t2, t3);
116
+ let t4;
117
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
118
+ t4 = { count, updateCountPostfix, updateCountPrefix };
119
+ $[4] = t4;
120
+ } else {
121
+ t4 = $[4];
122
+ }
123
+ return t4;
124
+}
125
+
126
+export const FIXTURE_ENTRYPOINT = {
127
+ fn: useFoo,
128
+ params: [],
129
+};
130
+
131
+```
132
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js
new
+42
@@ -0,0 +1,42 @@
1
+import {useRef, useEffect} from 'react';
2
+
3
+/**
4
+ * The postfix increment operator should return the value before incrementing.
5
+ * ```js
6
+ * const id = count.current; // 0
7
+ * count.current = count.current + 1; // 1
8
+ * return id;
9
+ * ```
10
+ * The bug is that we currently increment the value before the expression is evaluated.
11
+ * This bug does not trigger when the incremented value is a plain primitive.
12
+ *
13
+ * Found differences in evaluator results
14
+ * Non-forget (expected):
15
+ * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
16
+ * logs: ['id = 0','count = 1']
17
+ * Forget:
18
+ * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
19
+ * logs: ['id = 1','count = 1']
20
+ */
21
+function useFoo() {
22
+ const count = useRef(0);
23
+ const updateCountPostfix = () => {
24
+ const id = count.current++;
25
+ return id;
26
+ };
27
+ const updateCountPrefix = () => {
28
+ const id = ++count.current;
29
+ return id;
30
+ };
31
+ useEffect(() => {
32
+ const id = updateCountPostfix();
33
+ console.log(`id = ${id}`);
34
+ console.log(`count = ${count.current}`);
35
+ }, []);
36
+ return {count, updateCountPostfix, updateCountPrefix};
37
+}
38
+
39
+export const FIXTURE_ENTRYPOINT = {
40
+ fn: useFoo,
41
+ params: [],
42
+};
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -460,6 +460,7 @@ const skipFilter = new Set([
460
'fbt/bug-fbt-plural-multiple-function-calls',
461
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
462
'bug-invalid-phi-as-dependency',
463
+ 'bug-ref-prefix-postfix-operator',
464
465
// 'react-compiler-runtime' not yet supported
466
'flag-enable-emit-hook-guards',