react icon indicating copy to clipboard operation
react copied to clipboard

[Compiler Bug]: eslint-plugin-react-compiler errors when updating initialization of ref.current

Open jeremy-code opened this issue 1 year ago • 8 comments

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)
  • [X] 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/#N4Igzg9grgTgxgUxALhAHRFMCAEcA2AlggHYAuGA3GiTYQLYAOEMZOwOWCASggGY4Avjj4wI9HBhgIAhnAohqtEnAgkwbAKoA5AJJ6AKroCCAGV0AtAKIARHAF4cAZQCe9AEYR8ACgw79ukZmlrYYAJRKNAgAHsyseGoanNgA8iq4jgA8BgA0AHzehCSEZIQy+ABq5VAIyDgGOAA+ON7eYQ559WHt9p3ANDgJ6mzSAo5cvAKZ9U04ZC6MCBAC-oYm5tZ2nd6rgeshNhE0AziEAt6jAHRwsNLkDvaOu0Ebtu39JIODVzcwd2yOeaLZanYqlcpVfA1B6ODB8KAqUpqDA4AD8oJKZUq1QQbRwdSKmIhOKUg0Ex0+OGkZFgnx+t1IZCUgkiJBicTYABN+DIoPg2PDEYQ1DgALIuYyMRh4j7fBA0mCfTKcwgANzyAAkEPh8BAcAB1Fj4TmZAD0KvVzJoIEEQA

Repro steps

  1. Initialize useRef with some dummy value (e.g. null, Symbol, undefined, etc.) to be changed after initialization/during render.
  2. Update ref.current by checking whether it is equivalent to its initial condition (as per documentation: useRef#Avoiding recreating the ref contents)
  • To solve it, you may initialize the ref like this instead:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  // ...
  • Normally, writing or reading ref.current during render is not allowed. However, it’s fine in this case because the result is always the same, and the condition only executes during initialization so it’s fully predictable.

  1. eslint-plugin-react-compiler gives error
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)

How often does this bug happen?

Every time

What version of React are you using?

19.0.0-rc-1d989965-20240821

jeremy-code avatar Aug 21 '24 23:08 jeremy-code

Related: #30716

jeremy-code avatar Aug 21 '24 23:08 jeremy-code

Try this

import { useRef, useState, useEffect } from "react";

const UNINITIALIZED = Symbol("UNINITIALIZED");

export const useOnce = <T,>(initialValue: T | (() => T)) => { const ref = useRef < T | typeof UNINITIALIZED > (UNINITIALIZED); const [value, setValue] = useState < T | typeof UNINITIALIZED > (UNINITIALIZED);

useEffect(() => { if (ref.current === UNINITIALIZED) { const resolvedValue = typeof initialValue === "function" ? initialValue() : initialValue; ref.current = resolvedValue; setValue(resolvedValue); } else { setValue(ref.current); } }, [initialValue]);

return value; };

export default function MyApp() { return

Hello World
; }

State Initialization: Added a state value to store the initialized value.

useEffect: The ref is now initialized inside the useEffect hook, which sets the value both in the ref and in the state.

Return Value: The hook returns the state value, ensuring that the initial render doesn't access ref.current

parthnegi21 avatar Aug 28 '24 20:08 parthnegi21

Thanks for posting. This is a known limitation of the new linter.

josephsavona avatar Aug 31 '24 22:08 josephsavona

i also run into the problem in a different scenario: when passing the ref object to a custom hook the compiler is not optimizing my code. if i remove the ref access it works as expected.

// pass the ref object to a custom hook and it is preventing the optimizations.  
const mergedRefs = useMergedRef<HTMLDivElement>(ref, setPopperElement);

hlege avatar Sep 01 '24 21:09 hlege

Do you deprecated to use useRef? if not, how to use it?

const panelRef = useRef<HTMLDivElement>(null);
const width = panelRef.current?.clientWidth ?? 256; // eslint error here

iahu avatar Sep 02 '24 05:09 iahu

Is there any available workaround for this? This limitation also affects the useImperativeHandle.

alisherks avatar Sep 02 '24 06:09 alisherks

To anyone encountering optimization issues with the usage of useImperativeHandle, you can place the forwardedRef inside a useState hook.

const [refs] = useState(() => {
    return { forwardedRef };
});

This works against latest experimental release (0.0.0-experimental-4e0eccf-20240830). It probably breaks the rules, but so far didn't encounter serious issues.

alisherks avatar Sep 02 '24 08:09 alisherks

built an app to get paid for this PR https://www.n0va-io.com/discover/facebook/react

nkalpakis21 avatar Sep 05 '24 03:09 nkalpakis21

I think this is addressed in 19.0.0-beta-8a03594-20241020. I have a hook that looks like this:

export function useLogger(name: string) {
  const loggerRef = useRef<Logger>(null);
  if (loggerRef.current === null) {
    loggerRef.current = new Logger(name);
  }

  return loggerRef.current;
}

The linter rule complains only about that .current in the return statement. However, the conditional needs to have exactly this form of fooRef.current === null, otherwise the rule will report a false positive.

ravicious avatar Oct 22 '24 09:10 ravicious