-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[eslint-plugin-react-hooks] allow configuring custom hooks as "static" #16873
Comments
I went ahead and implemented this 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 To try out my fork:Install it Update your package.json to install - "eslint-plugin-react-hooks": "...",
+ "@grncdr/eslint-plugin-react-hooks": "5.0.0-p30d423311.0" Update your 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,
+ "useRouter": true,
+ "useEntityCache": true,
+ "useItem": [false, true],
+ "useQueryParam": [false, true],
+ "useSomeQuery": {
+ "reload": true,
+ "data": false,
+ "error": false,
+ "isLoading": false
+ }
+ }
+ }
+ ], (note the hook names above are specific to my app, you probably want your own) The
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 |
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. |
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 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 You can use it by installing |
This is really missing for us, because we have hooks like We have faced problems such as: const axios = useAxios(...);
const requestSomething = useCallback(() => {
return axios.get(...);
}, []); ESLint warning:
|
I’m curious about that use case: what is the useAxios hook doing that couldn’t be accomplished with a normal import? |
Internally it uses Additionally, it configures the |
I would also like to see this behavior, mostly just for @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 |
The 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
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! Limits of this proposalCouldn't we infer this automatically? Not using ESLint. Or alternatively, not without making this ESLint plugin extremely complicated. Even if somebody did that work there's no indication the React team at Facebook would want 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 Something like what we do with globals? Sorry if I'm wrong. |
Yep, that’s what this issue proposes and what I’ve 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’t 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:
Advantages of this approach:
Disadvantages of this approach: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. |
3 years almost but no progress on such a simple but useful addition. For those not wanting to use a fork, we can utilize patch-package to maintain our own patch locally. I created a patch out of @grncdr's work in his fork. |
This would be a great addition for all the written custom hooks. |
@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. |
|
For those who are interested, I've published a new version 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 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? |
I wrote an eslint plugin based on a forked version of @grncdr with some ideas from @kravets-levko.
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! |
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. |
No. The static value check option I created,
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 |
For visual explanation, my plugin with 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 |
Yes, of course! If any of the values in the dependency array are non-static (in the example above, if 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
// ...
}
If you're curious about the changes to the features I've added, check out this commit! |
Any update on this? because |
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 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!
The text was updated successfully, but these errors were encountered: