Skip to content

[Compiler Bug]: Incorrect JS output when copying and updating global variable with += in useEffect #34899

@cxcorp

Description

@cxcorp

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

https://playground.react.dev/#N4Igzg9grgTgxgUxALhAGwQFwAQEtsC82ADANwA6AdlQgB4AOEMOAZlJXJrhJdgIL16ACgCU2YFWzYoYBAFEWLBJyGjCAPnGSp2ODzA4Y7AHJQAtgCMEMQnm1S9lSBgB0aCAHMhAcgSLlhuzeADTYRpSmltYi9njYANREAIwUvDowWLC8agSaEmk6uvoQru5e3nAYAIaUUPRhQaHhkVYwMQXYAL7anaEA2gC67VIZmFnYADwAJrgAbuoA8gDSEwD0M-NU3ZQgwSCOLLgeKCC4ZozM2JgAnvQI4tgACmhQHriUC-Rc+l3YLDAQMzYbwWKpWNAAWnoLzelAhGSqnAhenOuAwMHWuAM3lSVCE+Skq1WKPoaKq30oAFkIFMEMhsOQQFU0GhGVtsGByVjDggwE8Ye9PhSwCJSLtwAALCAAdwAkpRMNZKMywCgWCqEJ0gA

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.

  1. 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 to runNumber before updating it for use in the cleanup function.

  2. Expected output in StrictMode:

    effect run 0
    cleanup run 0
    effect run 1
    
  3. Observed output:

    effect run 0
    cleanup run 1
    effect run 1
    
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bugType: Bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions