📎 Add `reactCompiler` option for `useExhaustiveDependencies`
Description
useExhaustiveDependencies currently reports a diagnostic when using functions that are not wrapped with useCallback as dependency: “[myFunc] changes on every re-render and should not be used as a hook dependency”.
This diagnostic is not accurate for users of the React Compiler, since it automatically wraps such functions.
To resolve this, I agree with this comment that we should introduce an option for this.
Before starting work on this, I think we need to agree what to call the option and where to place it. I think having a single reactCompiler option makes sense, so that if we need other changes to accommodate the React Compiler users still only need to set one option. But that leaves the question whether it should be on the useExhaustiveDependencies rule, or if other React-related rules may also benefit from this, in which case we may prefer to move it up. Suggestions welcome!
Would it be good enough to split useExhaustiveDependencies into two buckets, one with "[myFunc] changes on every re-render and should not be used as a hook dependency" warning, and another with whatever is left of useExhaustiveDependencies?
The first rule can be manually disabled for now. In the future, based on some heuristics we can disable it by-default when the react-compiler is detected.
@tusharsnx I think that's quite a sensible suggestion!
Note I'm also addressing your suggestion in #5486, since it might be a good use case for a reactCompiler domain.
can work on this one if the discussion can happen now and we get resolution :-)? just need to know what to implement
I just looked at the implementation, and I think splitting useExhaustiveDependencies might be quite a hassle. It would result in two rules that need to perform very similar analysis, so even if it can be cleanly refactored out, I don't think it would be efficient.
@vladimir-ivanov Feel free to also have a look for yourself, of course, but I'd say, let's stick with a simple option for now :) Assigning to you!
any suggestions on the name for this option please? @arendjr maybe hasReactCompiler or similar? or I also wonder if it is a terrible idea to use some sort of settings: {reactCompiler: on/ off} in biome.json, which can affect other biome rules too
Is there something I can do to have biome work in my project right now? We are using react 18, so we need support for deps included ie:
const myFn = useCallback(() => {}, [])
useEffect(myFn, [myFn]) // <-- we need this as dep in react 18
We are using biome 2 beta, and this is a bit of a blocker for us, we'd have to just disable it outright, which is not ideal.
also to confirm even if react-compiler is in scope (react 18+ AND eslint rule react-hooks is the rc version which detects it, it still throws the same error:
and eslint config
that said, it seems to be a simple change to disable function check IF react compiler option is set to true
@vladimir-ivanov Please remind how ESLint should be configured again for it to work properly. People should use a special import path?
@vladimir-ivanov Please remind how ESLint should be configured again for it to work properly. People should use a special import path?
https://gist.github.com/vladimir-ivanov/f3f22feee0708e79c93d2794b7b897ea
in what context please?
Thanks, already saw it in that snippet: 'react-hooks/react-compiler': 'error',
But you're saying that even configured like that, react-hooks/exhaustive-deps still gives the error message? Shouldn't that be a bug on their side?
any suggestions on the name for this option please? @arendjr maybe hasReactCompiler or similar?
Maybe reactCompilerEnabled?
Seems eslint/ react compiler: error option is NOT highlighting usage of useCallback as error. It might become the case at some point in time, as it is largely redundant in react 19 (react compiler). But then it does NOT highlight hook deps too :-), forgot about it.
If that is the case, then having reactCompilerEnabled on higher level would make sense. But within the current react hooks implementation - only thing that is affected is react-hooks/exhaustive-deps, so we start with an option within the useExhaustiveDependencies as per the title of this pr. and move it if necessary later?
thanks
We should also think about the DX as not all components can be memoized (due to their implementation). React compiler can statically check for this.
I'm proposing the following:
- All components must have implementation that allow for memoization
lint/noNonMemoizableComponent. - Component can opt out of the above rule using
biome-ignore. - Prevent memoization pritimitives (
useCallback,useMemo, etc) inside memoizing components. - Nothing changes for non memoizing component.
We should also think about the DX as not all components can be memoized (due to their implementation). React compiler can statically check for this.
I'm proposing the following:
- All components must have implementation that allow for memoization
lint/noNonMemoizableComponent.- Component can opt out of the above rule using
biome-ignore.- Prevent memoization pritimitives (
useCallback,useMemo, etc) inside memoizing components.- Nothing changes for non memoizing component.
personally think it will be confusing to introduce such an option, if I see such an option I will understand it as something to do with React.memo.
personally think it will be confusing to introduce such an option, if I see such an option I will understand it as something to do with React.memo.
Yes, it could be confusing. We should try something more specific, maybe prefix react compiler?
Also, just noticed React has an official way to opt in and opt out of memoization. So, we shouldn't need any biome specific directives for opting.
@tusharsnx yes, you are right, we also should consider these use no memo/ use memo etc
hm, did some more investigation on react-hooks/react-compiler rule, looking at the src code it ONLY reports if use memo/ use no memo and use forget/ use no forget should be used or not. plugin src code, rule$1 = react-hooks/react-compiler
now the default value of react compiler is an interesting beast, it is set in babel/ or vite or package.json -> babel prop etc as:
['babel-plugin-react-compiler', { compilationMode: 'all', runtimeModule: 'react-compiler-runtime' }]
where compilationMode can be one of:
-
"annotation" (default) Only compiles components and hooks that have explicit 'use memo' directives Most conservative approach, requires manual opt-in for each component
-
"infer" Automatically infers which components should be memoized based on heuristics The compiler analyzes your code and decides what to optimize More automatic than annotation mode but less aggressive than "all"
-
"all" (what you're currently using) Compiles all components and hooks by default Most aggressive optimization mode Components can opt-out using directive 'use no memo'
ESLint may have to be configured to pick up babel config to read the compilationMode default
NOTE:
its setting does NOT affect in any ways exhaustive deps, it behaves exactly as before.
my suggestion @arendjr we leave useExhaustiveDependencies as is and if anything we introduce another rule reactCompiler which does read compilationMode from babel (like eslint) and highlights the 'use memo' or 'use no memo' directive if it is already the default.
@vladimir-ivanov I agree that starts to sound like the most sensible path indeed. But does that mean users use a useReactCompiler instead of useExhaustiveDependencies?
@arendjr not really, they do complement each other as per design and are independent of each other:
useExhaustiveDependencies does what it is doing right now - flags the need of an additional hook,
useReactCompiler (depending on compilationMode setting):
or if not all it will show the same message BUT for 'use memo'
and does not affect anything else.
e.g. useExhaustiveDependencies regardless of the value of compilatoinMode will show as before:
Else I can see the temptation of having reactCompilerEnalbed option on useExhaustiveDependencies (but lets see what react decides to do with compiler going forward first).
thanks
Ah, thanks! It seems I was under the impression that the "changes on every render" error was eliminated entirely when using the React compiler, but that seems not the case then?
yes, and for the record the snippets above are based on "eslint-plugin-react-hooks": "^6.0.0-rc.1", as it may change in the future :-)
https://github.com/biomejs/biome/pull/6187 raised this pr, but will close it now :-)
shall we close this issue please? or else please un assign from myself
shall we close this issue please?
I think we should wait for some time. The plans around how useEffect will change due to the compiler are still unclear. There are good reasons (here, here, and here) to believe that the React team has alternative plans for useEffect. Changing useExhaustiveDependencies behaviour at this point might be too early.
One neat thing about biome is that you can silence the value changes on every re-render and should not be used as a hook dependency warning without silencing the other two related but important warnings: This hook specifies more dependencies than necessary and This hook does not specify all of its dependencies. This is more or less what we want from an option like reactCompiler to do globally. But for the time being, this is a "good-enough" workaround.
You can silence the warning like this:
function Component({ prop }) {
"use memo"
const isConnected = prop === "connected";
useEffect(() => {
if (!isConnected) {
return;
}
// do something
// This safely ignores the "...changes on every render..." warning only:
// biome-ignore lint/correctness/useExhaustiveDependencies:
}, [isConnected]);
}