Open grncdr opened 5 years ago
@grncdr Do you have your fork published anywhere? I'd love to see how you handled it, and it'd be a great to dust it off and get an official PR here.
@grncdr Do you have your fork published anywhere? I'd love to see how you handled it, and it'd be a great to dust it off and get an official PR here.
https://github.com/facebook/react/issues/16873#issuecomment-592574971
For those who are interested, I've published a new version 5.0.0-p30d423311.0
that is rebased on top of the latest upstream changes.
The main improvement is fixing compatibility with ESLint 8. Thanks a lot to @luketanner for not only bringing this to my attention, but opening a PR to fix it. 🥇
I'd very much like to see the @grncdr enhancement merged into the trunk -- ideally with some configuration so that one could generalize this pattern and declare a whole family of custom hooks that guarantee stable values. (There are plenty of ways to wrap useState
, useEvent
et al!)
Until then, I'll look into using your fork; thanks for sharing.
Could someone enlighten me what the downside is of adding a static dependency to the dependency array of a hook? The way I understand it, one can add the static dependency to the array and it shouldn't have any effect and it will appease the linter rule
@deiga There isn't a functional difference, but it is also unnecessary and leads to confusion for a moment. The linter allows omitting internally known list of hooks from deps array and we would like to be able to configure that list.
+1 in favour of this configuration option, needed for:
Bump. This is desperately needed. This issue has been a massive blocker to wide use of custom hooks.
Any progress?
IMHO it would be great if this plugin could detect some common "static" patterns in custom hook, for example if custom hook returns result of
useRef()
/useCallback(..., [])
/useMemo(..., [])
etc.
I wrote an eslint plugin based on a forked version of @grncdr with some ideas from @kravets-levko. https://github.com/wogns3623/eslint-plugin-better-exhaustive-deps
This is way beyond the scope of what ESLint can do (it would require whole-program taint-tracking) so definitely not going to happen here.
The static value tracking I wrote was based on existing code from the React team, so I don't think it will have the performance issues @grncdr was worried about - feel free to correct me if I'm wrong!
P.S. English is not my native language, so the sentences may seem strange. Please let me know of any strange things and I'll fix them!
I wrote an eslint plugin based on a forked version of @grncdr with some ideas from @kravets-levko. https://github.com/wogns3623/eslint-plugin-better-exhaustive-deps
Nice, and it's probably a good idea to write it as a separate plugin instead of a fork of the facebook one, since it seems there's no interest from the facebook team.
I took a quick look at the repo but since it starts with an initial commit that already has the relevant changes I'm not really sure what you did differently. Do I understand correctly that this can handle situations like the following?
// file: use-toggle.js
import {useCallback, useState} from 'react'
export function useToggle(initialState = false) {
const [state, setState] = useState(initialState)
const toggle = useCallback(() => setState(x => !x), [])
return [state, toggle, useState]
}
// file: some-component.js
import { useCallback } from 'react'
import { useToggle } from './use-toggle.js'
export function SomeComponent() {
const [enabled, toggleEnabled] = useToggle()
// ignore how contrived this callback is, the important thing is that
// `toggleEnabled` is detected as static.
const toggleAndLog = useCallback(() => {
toggleEnabled()
console.log('toggled state')
}, [])
return <div>
<div>{enabled ? 'Enabled' : 'Disabled'}</div>
<button onClick={toggleAndLog}>Toggle</button>
</div>
}
That is, does it propagate "staticness" across files?
In any case, I'm hardly doing anything with React these days, so I'd be very happy to pass the torch and recommend your plugin over my forked one.
That is, does it propagate "staticness" across files?
No. The static value check option I created, checkMemoizedVariableIsStatic
, was created to check if the return value of useMemo
or useCallback
is static within a single component. It still doesn't automatically infer whether the return value of a custom hook written outside of the component is static, and you still have to manually write the staticHooks
option for that.
IMHO it would be great if this plugin could detect some common "static" patterns in custom hook, for example if custom hook returns result of useRef()/useCallback(..., [])/useMemo(..., []) etc.
Certainly I misunderstood the meaning you discussed due to my poor English. I agree with your comment that it's hard to automatically infer the staticness of the return value of a hook written outside of a component (though I'll try to find a way).
I added the checkMemoizedVariableIsStatic
function because I thought there was no way to recognize the return value of form like useCallback(..., [])
as static, even though useCallback
/useMemo
are the default hooks provided by react.
For visual explanation, my plugin with checkMemoizedVariableIsStatic: true
can handle the following cases.
import { useCallback } from "react";
export function SomeComponent() {
const [enabled, setEnabled] = useState(false);
const toggleEnabled = useCallback(() => setEnabled((x) => !x), []);
// `toggleEnabled` is detected as static.
const toggleAndLog = useCallback(() => {
toggleEnabled();
console.log("toggled state");
}, []);
return (
<div>
<div>{enabled ? "Enabled" : "Disabled"}</div>
<button onClick={toggleAndLog}>Toggle</button>
</div>
);
}
That still seems like a solid improvement. Does it take into account the dependencies array of useCallback
so that useCallback(() => {...}, [someProp])
is not considered static?
Yes, of course! If any of the values in the dependency array are non-static (in the example above, if someProp
is non-static), then the return value will be non-static. If the values are all static, the return value will also be treated as static (although there's no reason to do this).
import { useEffect, useCallback, useMemo } from 'react';
export function SomeComponent({ nonStaticValue }: { nonStaticValue: string }) {
const staticValue = 'some static value';
// `staticCallback` is treated as static.
const staticCallback = useCallback(() => {
console.log(staticValue);
// if dependency array has static value(even if it is not necessary),
// return value of useCallback treated as static.
}, [staticValue]);
// if any of the dependencies are not static, return value of useCallback or useMemo treated as non-static.
const someMemoizedValue = useMemo(() => {
return staticValue + nonStaticValue;
}, [nonStaticValue]);
useEffect(() => {
console.log(someMemoizedValue);
}, []);
// ^^ lint error: React Hook useEffect has a missing dependency: 'someMemoizedValue'.
useEffect(() => {
staticCallback();
}, []); // no lint error
// ...
}
I took a quick look at the repo but since it starts with an initial commit that already has the relevant changes I'm not really sure what you did differently. Do I understand correctly that this can handle situations like the following?
If you're curious about the changes to the features I've added, check out this commit!
Any update on this? because biome
already implemented that https://github.com/biomejs/biome/pull/1979 . Guess I gotta think about switch to biome
then :/
Here was my solution for today - getting out of the hooks business altogether for things that are "configured once" during app initialization such as maybe an axios
instance or whatever. Instead of using a hook to get these things into my components, I'm now using a main module to configure first and then do an async import(s) for components that need the configured things.
The asyncly imported components can then just import the pre-configured things they need at module level instead of using a hook at all.
I've tested this with my own app and it works great. I think it will be even better than involving hooks for that sort of thing, so I hth.
Do you want to request a feature or report a bug?
Feature/enhancement
What is the current behavior?
Currently the eslint plugin is unable to understand when the return value of a custom hook is static.
Example:
What is the expected behavior?
I would like to configure
eslint-plugin-react-hooks
to tell it thattoggleEnabled
is static and doesn't need to be included in a dependency array. This isn't a huge deal but more of an ergonomic papercut that discourages writing/using custom hooks.As for how/where to configure it, I would be happy to add something like this to my .eslintrc:
Then the plugin could have an additional check after these 2 checks that tests for custom names.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
All versions of eslint-plugin-react-hooks have the same deficiency.
Please read my first comment below and try my fork if you are interested in this feature!