Skip to content

[Compiler]: unnecessary memoization of non-primitive arguments in native React hooks #33031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 of 4 tasks
szhsin opened this issue Apr 26, 2025 · 1 comment
Open
1 of 4 tasks

Comments

@szhsin
Copy link

szhsin commented Apr 26, 2025

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/#N4Igzg9grgTgxgUxALhASwLYAcIwC4AEwBUYCASggGYA0JZAyngIZ4IEC+BVMEGBAchgJmcPAIDcAHQB2suBBlhCpBAz4I8ACzQyA5gQC8BABTEAbswA2Udh2REClmwgcyoGAEYIYnAJRGAHxEsgQECkqEwlRG9BTUZk7Wtv7SMmERygQA2sqsCHRkeEz5ALqxqiVsic4pHH5poQTCeLDpwE1h0TSdBHlsPelhfZpVCE0caZOysggAHjj4jpUa2roG0zIgHEA

Repro steps

Currently, the React compiler memoizes parameters passed to hooks if those parameters are non-primitives. This behavior appears to be applied universally, regardless of whether the hook is a built-in (native) React hook or a custom one.

However, for most native hooks, this memoization is unnecessary. It results in increased output code size and adds runtime overhead without any meaningful benefit.

Examples

Input code using useRef:

const ref = useRef({ value });

Compiled output:

  let t1;
  if ($[0] !== value) {
    t1 = { value };
    $[0] = value;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  const ref = useRef(t1);

Input code using useState:

const [state, setState] = useState({ value });

Compiled output:

  let t2;
  if ($[2] !== value) {
    t2 = { value };
    $[2] = value;
    $[3] = t2;
  } else {
    t2 = $[3];
  }
  const [state, setState] = useState(t2);

In both examples, the memoization of the initial values passed to the native hooks is unnecessary.

This same pattern of memoization is also applied to other built-in hooks such as useReducer and useEffect

Expected behavior

The compiler should treat native hooks differently and avoid memoizing parameters when it's not needed. This would reduce both the generated code size and runtime overhead.

How often does this bug happen?

Every time

What version of React are you using?

19.1.0

What version of React Compiler are you using?

19.1.0-rc.1

@szhsin szhsin added Component: Optimizing Compiler Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug Type: Bug labels Apr 26, 2025
@josephsavona josephsavona removed Type: Bug Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug labels May 2, 2025
@josephsavona josephsavona changed the title [Compiler Bug]: unnecessary memoization of non-primitive arguments in native React hooks [Compiler]: unnecessary memoization of non-primitive arguments in native React hooks May 2, 2025
@josephsavona
Copy link
Member

Thanks for posting about this. You're right that the inputs to builtin React hooks don't have to be memoized. However, it is not free to produce these values and memoization can still be beneficial. We'd want to run some benchmarking and experiments to see what the right tradeoff is here.

The actual change itself would be trivial, we already have infrastructure to mark function or hook declarations as not aliasing their argument (noAlias internally) which we could apply to the builtin hook declarations to stop memoizing their inputs. But again, we're not sure if this is actually the best tradeoff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants