[Compiler Bug]: eslint-plugin-react-compiler errors when updating initialization of ref.current
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
- Initialize
useRefwith some dummy value (e.g. null, Symbol, undefined, etc.) to be changed after initialization/during render. - Update
ref.currentby 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.
- 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
Related: #30716
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
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
Thanks for posting. This is a known limitation of the new linter.
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);
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
Is there any available workaround for this? This limitation also affects the useImperativeHandle.
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.
built an app to get paid for this PR https://www.n0va-io.com/discover/facebook/react
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.