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:
import React from 'react'
function useToggle(init = false) {
const [state, setState] = React.useState(init)
const toggleState = React.useCallback(() => { setState(v => !v) }, [])
return [state, toggleState]
}
function MyComponent({someProp}) {
const [enabled, toggleEnabled] = useToggle()
const handler = React.useCallback(() => {
toggleEnabled()
doSomethingWithTheProp(someProp)
}, [someProp]) // exhaustive-deps warning for toggleEnabled
return <button onClick={handler}>Do something</button>
}
What is the expected behavior?
I would like to configure eslint-plugin-react-hooks
to tell it that toggleEnabled
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:
{
"staticHooks": {
"useToggle": [false, true], // first return value is not stable, second is
"useForm": true, // entire return value is stable
}
}
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.
I went ahead and implemented this (see above commit) just to see how it would play out in my own codebase. If anybody else feels like trying it, I've published it to npm under @grncdr/eslint-plugin-react-hooks
. You will need to update your eslintrc to reference the scoped plugin name and configure your static hook names:
- "plugins": ["react-hooks"],
+ "plugins": ["@grncdr/react-hooks"],
"rules": {
- "react-hooks/rules-of-hooks": "error",
- "react-hooks/exhaustive-deps": "warn",
+ "@grncdr/react-hooks/rules-of-hooks": "error",
+ "@grncdr/react-hooks/exhaustive-deps": [
+ "error",
+ {
+ "additionalHooks": "usePromise",
+ "staticHooks": {
+ "useForm": true,
+ "useEntityCache": true,
+ "useItem": [false, true],
+ "useQueryParam": [false, true]
+ }
+ }
+ ],
_(note the hook names above are specific to my app, you probably want your own)_
If anybody from the React team thinks the idea is worth pursuing I'll try to add some tests and make a proper PR.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.
This seems like a really great addition, would love to see it in react-hooks
@VanTanev have you tried my fork? I've been using it since my last comment and haven't had any issues, but positive experience from others would presumably be interesting to the maintainers.
Any news on this. It's very annoying now because you cannot use reliably this lint rule when you use custom hook, so you have to disable the rule leading to potential dangerous situations
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.
Indeed. Still there may be ambiguous situations and so having the ability to set it up through options could still be needed
Commenting to bump this thread and show my interest. Working on a large codebase with lots of custom hooks means that this would allow us to more reliably use the hooks linter. I understand that the reason they might not want to allow this, is because it could enable people to introduce dangerous bugs into their apps. So maybe it's a feature that should be added with a large disclaimer.
It's pretty likely that the maintainers simply don't want to deal with bug reports that are related to people setting their hooks to static when they actually aren't static. A lot of people will misunderstand what it means to have static hooks.
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.
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.
I understand that the reason they might not want to allow this, is because it could enable people to introduce dangerous bugs into their apps. So maybe it's a feature that should be added with a large disclaimer.
It's pretty likely that the maintainers simply don't want to deal with bug reports that are related to people setting their hooks to static when they actually aren't static. A lot of people will misunderstand what it means to have static hooks.
I would totally understand this point of view, but until somebody from the React team replies, I'll keep hoping (and using my fork 馃槈).
@grncdr can you please point me to the source of your folk?
@ksjogo sure, my changes are in this branch: https://github.com/grncdr/react/tree/eslint-plugin-react-hooks-static-hooks-config
This is really missing for us, because we have hooks like useAxios
that always return the same value.
We have faced problems such as:
const axios = useAxios(...);
const requestSomething = useCallback(() => {
return axios.get(...);
}, []);
ESLint warning:
React Hook useCallback has a missing dependency: 'axios'. Either include it or remove the dependency array.eslint(react-hooks/exhaustive-deps)
I鈥檓 curious about that use case: what is the useAxios hook doing that couldn鈥檛 be accomplished with a normal import?
I鈥檓 curious about that use case: what is the useAxios hook doing that couldn鈥檛 be accomplished with a normal import?
Internally it uses useMemo
to create an instance of axios, and also a useEffect
that cancels pending requests when the component is unmounted.
Additionally, it configures the baseUrl
and automatically injects the authentication token via interceptor.
I would also like to see this behavior, mostly just for setState
and useRef
.
@douglasjunior don't want to get too off-topic, but you might just wanna have a global/singleton/etc. for that? Seems unnecessary to set the baseUrl
and token every time you use the hook, as presumably those values will not change between instances of the hook.
@douglasjunior don't want to get too off-topic, but you might just wanna have a global/singleton/etc. for that? Seems unnecessary to set the
baseUrl
and token every time you use the hook, as presumably those values will not change between instances of the hook.
The useAxios
is configurable, it can receive a custom baseURL, and others configs.
But in the end it makes no difference, the main purpose for us is to cancel pending requests, and make the axios instance private to the component.
Allowing configuration of the dependency argument position would be useful as well.
It is currently hard coded to 0
for additionalHooks
:
https://github.com/facebook/react/blob/8b580a89d6dbbde8a3ed69475899addef1751116/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js#L1361
This allows support for hooks that take more than 2 arguments. Eg.:
useImperativeHandle(ref, callback, deps)
I've separately implemented something along the lines of:
rules:
customHookDeps:
- error
- additionalHooks
- useEventListener: 1
- useCustomHook: 0
- useOtherHook
Where the regex syntax can still be supported.
Food for thought: if ESLint is able to leverage any TypeScript information, there could be a way to type-level annotate hooks accordingly.
I think this discussion would benefit from some clarification of what is possible and what is feasible. To that end, I'm writing below the limits on what I would personally propose. I certainly don't know everything about what can be done with ESLint, so if you read this and think "he doesn't know what he's talking about" please correct me!
Couldn't we infer this automatically?
Not using ESLint. Or alternatively, not without making this ESLint plugin extremely complicated. And even if somebody did that work (and the team was willing to maintain it)
Could we annotate the "staticness" of a hook in the source code? (using types and/or comments)
Unfortunately no, the reason is that the ESLint plugin must analyze the locations a variable is _used_ and not where it's declared. At minimum, you would need to annotate a hook every time you import it, since ESLint works on a file-by-file basis.
Could a type checker do this automatically?
After reading the above you might think that Typescript or Flow could be leveraged to tell us when a return value is static. After all, they have the global information about values in separate modules that a linter doesn't.
However, neither of them (as far as I'm aware) let you talk about the type of the _implicit environment_ created by a closure. That is, you can't refer to the variables captured by a function. Without this, you can't propagate information about the closed-over variables to the return type. (If the type systems did have this capability, you theoretically wouldn't need to write the dependency arrays at all)
--
I think it is possible to pass a parameter to "eslint-plugin-react-hooks" in .eslintrc
, with the names of the hooks that are static?
Something like what we do with globals?
Sorry if I'm wrong.
I think it is possible to pass a parameter to "eslint-plugin-react-hooks" in
.eslintrc
, with the names of the hooks that are static?
Yep, that鈥檚 what this issue proposes and what I鈥檝e implemented (see my earlier comments for details). I just wanted to clarify that I think the explicit configuration makes the best possible trade off in terms of implementation complexity.
I think it would be great to have this. Anyone know how to get feedback from a maintainer to see if we can move forward with this?
Suggestion: Follow the same idea as the "camelcase" rule and add "static" option.
@douglasjunior could you provide an example of what you mean? I didn鈥檛 understand what you wanted to demonstrate with the PR you linked.
I'm a little late to the party but I think a better approach would be to infer such cases by tracing the retuned values and see if it's something static. If this is not feasible, or doesn't make sense from a performance point of view, maybe we can at least annotate each custom hook to provide such information in a jsdoc comment block like this:
/**
* Inspired from the format that is suggested in the discussions above:
* @react-hook: [false, true]
*/
function useToggle(init = false) {
const [state, setState] = React.useState(init);
const toggleState = React.useCallback(() => {
setState(v => !v);
}, []);
return [state, toggleState];
}
In the meanwhile that third-party libraries adopt this feature, there is no way to teach eslint-plugin-react-hooks about such static stuff. i.e. the same advantage of being able to put this information in the code can become a disadvantage when you don't have access to the code and it doesn't include this information for any reason.
@alirezamirian do you know if ESlint makes it possible/easy to get the AST information for imported modules? I was under the impression it only worked on a single file at a time.
@grncdr That's a good point. I'm not an eslint expert but I think you are right and we only have Access to Program node corresponding to a single file. The best we can get from the AST in hand is the import statement source. I don't know if there is a utility for resolving AST from an import declaration.
UPDATE: There is a parserPath
property on the context
, which has a parseForEslint
function which can be used to parse other files. So it's technically feasible. But I'm not sure if it's a right approach and it's meant to be used like this.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
bump
Just another "+1" post, but I'd like to add in that, while there are workarounds, such as using refs or hooks to wrap that kind of logic, it feels unnecessarily boilerplate-y. Having a pragma for ignored values would be so valuable--often devs lazily and dangerously just turn off the whole block and loose safety.
Most helpful comment
bump