-
Notifications
You must be signed in to change notification settings - Fork 49.6k
Description
What kind of issue is this?
- React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- react-compiler-healthcheck (build issue installing or using the healthcheck script)
Link to repro
Repro steps
In a useEffect
, when capturing a copy of a global variable, then incrementing the global variable with an AssignmentExpression (i += 1
), React Compiler optimizes away the copied variable and instead changes to reference the global variable directly.
-
Create a component like:
let i = 0; function App() { useEffect(() => { const runNumber = i; console.log("effect run", runNumber); i += 1; return () => { console.log("cleanup run", runNumber); }; }, []); return <div>OK</div>; } export function Main() { return <StrictMode><App /></StrictMode> }
Here we specifically capture a copy of
i
torunNumber
before updating it for use in the cleanup function. -
Expected output in
StrictMode
:effect run 0 cleanup run 0 effect run 1
-
Observed output:
effect run 0 cleanup run 1 effect run 1
-
Check compiler output:
import { c as _c } from "react/compiler-runtime"; let i = 0; export function App() { const $ = _c(2); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = []; $[0] = t0; } else { t0 = $[0]; } useEffect(_temp2, t0); let t1; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { t1 = <div>OK</div>; $[1] = t1; } else { t1 = $[1]; } return t1; } function _temp2() { console.log("effect run", i); i = i + 1; return _temp; } function _temp() { console.log("cleanup run", i); }
As you can see in the bottom at
_temp2
and_temp
, the compiler has optimized away the copy (runNumber
) and produces an incorrect result.
If changing to use i++
instead of i += 1
i.e. an UpdateExpression
, React Compiler correctly bails out (playground):
Found 2 errors:
Todo: (BuildHIR::lowerExpression) Support UpdateExpression where argument is a global
5 | const runNumber = i
6 | console.log('effect run', runNumber)
> 7 | i++;
| ^^^ (BuildHIR::lowerExpression) Support UpdateExpression where argument is a global
8 | return () => {
9 | console.log('cleanup run', runNumber)
10 | }
Todo: (BuildHIR::lowerExpression) Support UpdateExpression where argument is a global
5 | const runNumber = i
6 | console.log('effect run', runNumber)
> 7 | i++;
| ^^^ (BuildHIR::lowerExpression) Support UpdateExpression where argument is a global
8 | return () => {
9 | console.log('cleanup run', runNumber)
10 | }
This pattern is pretty common when showing how exactly StrictMode's effect->cleanup->effect works, but you can imagine my surprise when I saw that React had time traveled and cleaned up the second effect run before the effect even ran.
How often does this bug happen?
Every time
What version of React are you using?
react@19.2.0
What version of React Compiler are you using?
babel-plugin-react-compiler@1.0.0