-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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] Feedback for 'exhaustive-deps' lint rule #14920
Comments
This is an uncontrolled Checkbox component which takes a The rule complains that I don't see any way of distinguishing between this and the type of case that the rule is meant to catch, but it would be great if the final rule documentation could include a suggested workaround. 🙂 |
Re: #14920 (comment) @billyjanitsch Would this work instead? https://codesandbox.io/s/jpx1pmy7ry I added |
So I got this following (edge?) case where I pass some html and use it with The idea is to intercept all the link clicks from the editorial content and track some analytics or do other relevant stuff. This is a watered down example from a real component: |
This comment has been minimized.
This comment has been minimized.
const onSubmit = React.useCallback(
() => {
props.onSubmit(emails);
},
[emails, props]
); I expect the lint to fix the deps into
https://codesandbox.io/s/xpr69pllmz
A user will add an email and invite those email to the app. I intentionally remove the rest of the UI to just the
My component has a single prop, EDIT: Answered in #14920 (comment)
|
It doesn't consider that mount and update can be distinctly different when integrating when 3rd party libs. The update effect can't be included in the mount one (and removing the array altogether), because the instance shouldn't be destroyed on every render |
This comment has been minimized.
This comment has been minimized.
@joelmoss @sylvainbaronnet I appreciate your feedback but I hid your comments because they didn't include the information we asked for at the top of the issue. That makes this discussion unnecessarily difficult for everyone because there's missing context. I'd be happy to continue the conversation if you post again and include all the relevant information (see the top post). Thank you for understanding. |
I believe the code as-is shouldn't error. It does right now on line 35, saying that |
I totally understand, and I would normally do all that for you, but in my specific case, none of the code is unique to me. It's simply a question about whether the use of a function that is defined outside of the hook (ie. a redux action creator) and used within a hook should require that function to be added as a hook dep. Happy to create a codesandbox if you still need more info. thx |
@joelmoss I think my repro covers your case too. |
Yes, a CodeSandbox would still help. Please imagine what it's like to context switch between people's code snippets all day. It's a huge mental toll. None of them look the same. When you need to remember how people use action creators in Redux or some other concept external to React it's even harder. The problem may sound obvious to you but it's not at all obvious to me what you meant. |
@gaearon I understand it makes sense, actually it worked for me because why I wanted to achieve was :
Thanks a lot for the clarification. (and sorry for the off topic) |
Here's the code version of what I'm implementing. It don't look like much, but the pattern is 100% identical with my real code. https://codesandbox.io/s/2x4q9rzwmp?fontsize=14
Everything works great in this example! Except the linter issue.
I've got several files like this, where I'm executing a function in the effect, but I only want it to run whenever some condition is met - for instance, changing the series Id. I don't want to include the function in the array. This is what I get from the linter when running it on the sandbox code:
My question is: is this how it should behave (either the linter rule, or the hook array rule)? Is there a more idiomatic way of describing this effect? |
@svenanders, I'm curious about the reason why you don't want to include |
For one - it's about clarity. When I look at the function, I want to easily understand when it should fire. If I see one id in the array, it's crystal clear. If I see that id and a bunch of functions, it becomes more muddled. I have to spend time and effort, possibly even debugging functions, to understand what's going on. |
https://codesandbox.io/s/4xym4yn9kx
The user accesses a route on the page, but they're not a super user, so we want to redirect them away from the page. The Everything works!
I put in the dependencies correctly as in the code sandbox, but the linter is telling me that the dependency list should have A tweet with screenshots! https://twitter.com/ferdaber/status/1098966716187582464 EDIT: one valid reason this could be buggy is if |
CodeSandbox: https://codesandbox.io/s/711r1zmq50 Intended API:
Steps:
IMO adding "everything" we use in dependencies array is not efficient.For example consider the useDebounce Hook. In real world usages (at least in ours), the
I think adding everything in dependencies array, has the same (or even worse) performance issue as using pure components everywhere. Since, there are many scenarios in which we know that some of the values we're using will not change, so we shouldn't add them in dependencies array, because as @gaearon said, shallow equality checks are not very cheap and this can make our app slower. |
I have some feedback about enabling this rule to automatically work on custom hooks by convention. I can see in the source code that there is some intent to allow people to specify a regex to capture custom hooks by name: react/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js Lines 490 to 492 in ba708fa
What would the React team think about an additional naming convention for custom hooks with dependency arrays? Hooks already follow the convention of being prefixed by The plugin could still additionally support regexes, but I think I'd see great benefit by allowing library authors to simply export their custom hooks postfixed with |
This comment has been minimized.
This comment has been minimized.
I'd be fine to add it to the dependencies array. In our particular business case, this can never happen but I guess it's a good idea to add it anyway.
That's exactly the issue we have, too :/ In our implementation, we've even use an additional property to make it easier to set the body so we would have to use 2 |
How can you run a useEffect which has dependencies only once without the rule complaining when passing an empty array? Say, something like:
|
I wouldn’t say these are implementation details. It’s your API. If an object is passed we assume it can change any time. If in practice it’s always static, you can make it an argument to a Hook factory. Like I understand it’s a bit weird. We have a similar problem with the In practice — we’ll be looking more at this in the future. But you shouldn’t treat a potentially dynamic object as a static one because then a user can’t change it. |
@asylejmani You haven’t provided any details about your use cases, as requested in the top post. Why do you expect that someone can provide an answer your question? The point of the rule is to tell you that In both cases it’s not clear what you are asking. The rule complains that things can change, and you don’t handle that change. Without a full example, you alone can answer why you think handling it is not necessary. |
@gaearon sorry for not being clearer (it always sounds clear in one's head) :) my question is more like how would you achieve componentDidMount with useEffect. I was under the impression than an empty array does that, but the rule is telling me to always include dependencies in the array. |
@asylejmani I think the biggest gotcha with class life cycle methods like
Number 1 is where you put the logic in The rule is complaining you are not doing the number 2 part of the flow |
I think me and @asylejmani are on the same page here, but I guess what you are saying @gaearon is that we are probably wrong to only run the effect on mount if we actually have dependencies. Sorry for not providing a sandbox yet. I started the other night with a Create React App example, couldn't find out how to run eslint on the sandbox, and then lost the sandbox when reloading the browser without saving first (assumed CodeSandbox temp stored, my bad). In any case, I get what you are saying, and that under normal scenarios it is probably best to include those dependencies and not assume it's enough to run on mount only. There are probably valid use-cases for that too though, but it's a bit hard to explain or come up with a good example, so I'll live with disabling the rule inline when needed. |
@asylejmani is your use case similar to #14920 (comment)? I don't think it's possible for the rule to understand the scenario in this case, so we'll just need to manually disable it for that type of code. In all other cases the rule works as it should. |
Not sure if this makes sense, but one scenario that's quite common for me is something like this:
Both of these dependencies are coming from redux. The data (in this case) should only be loaded once, and the action creator is always the same. This is specific to redux, and your eslint rule cannot know this, so I get why it should warn. Still wondering if providing an empty array should perhaps just disable the rule? I like that the rule tells me about missing deps if I provided some but not all, or if I didn't provide any at all. Empty array means something different to me. But that might just be me :) Thanks for all your hard work! And for making our lives as developers better :) |
My use case is much simpler and I can of course add all the dependencies and it will still work the same, however I was under the impression that the rule will "warn" when you have some dependencies but missing others. @einarq's use case is something I used a few times, for example on "componentDidMount" load the data if there is none (from redux or whatever). I also agree that in these cases disabling the rule inline is the best choice. That case you know exactly what you're doing. I believe my whole confusion was [] vs [some], and of course thanks @gaearon for the awesome work :) |
Yes. If your component doesn't handle updates to a prop, it is usually buggy. The design of
If it's the same then including it in the dependencies won't hurt you. I want to emphasize it — if you're sure your dependencies never change then there is no harm in listing them. However, if later it happens that they change (e.g. if a parent component passes different function depending on state), your component will handle that correctly.
No. Providing an empty array and then wondering why some props or state is stale is literally the most common mistake. |
Makes a ton of sense, thanks
… On 8 Mar 2019, at 15:27, Dan Abramov ***@***.***> wrote:
I think me and @asylejmani are on the same page here, but I guess what you are saying @gaearon is that we are probably wrong to only run the effect on mount if we actually have dependencies. Is that a fair statement?
Yes. If your component doesn't handle updates to a prop, it is usually buggy. The design of useEffect forces you to confront it. You can of course work around it, but the default is to nudge you to handle these cases. This comment explains it well: #14920 (comment).
Both of these dependencies are coming from redux. The data (in this case) should only be loaded once, and the action creator is always the same.
If it's the same then including it in the dependencies won't hurt you. I want to emphasize it — if you're sure your dependencies never change then there is no harm in listing them. However, if later it happens that they change (e.g. if a parent component passes different function depending on state), your component will handle that correctly.
Still wondering if providing an empty array should perhaps just disable the rule?
No. Providing an empty array and then wondering why some props or state is stale is literally the most common mistake.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
I'm not sure what this means. Can you provide an example? |
We did a pass over these with @threepointone today. Here's a summary: Fixed in the Lint RuleExtraneous
|
@gaearon , thanks for taking the time to dive into the issues and summarizing action items into different categories. You missed my sample (#14920 (comment) by @trevorgithub) in your closing summary comment. (I certainly appreciate there was a lot of feedback from a lot of people; I think my original comment got lost in the hidden items section somewhere in the middle of the issue comments). I'm assuming my sample would fall under 'Integrating with Imperative/Legacy Code', although maybe other categorie(s) as well? In the case of 'Integrating with Imperative/Legacy Code' issues, it sound like there may not be a lot that can be done. In those cases, how would one ignore this warning? I guess: |
Sorry I missed this one. If you receive some data via props but don’t want to use those props until some explicit change, it sounds like a correct way to model it would be to have derived state. You’re thinking about it as “I want to ignore a change to a prop until another prop”. But you can also think about it as “My component has fetching function in the state. It’s updated from a prop when another prop changes.” Generally derived state is not recommended but here it seems like what you want. The simplest way to implement this would be something like: const [currentGetData, setCurrentGetData] = useState(getData);
const [prevRefreshRequest, setPrevRefreshRequest] = useState(refreshRequest);
if (prevRefreshRequest !== refreshRequest) {
setPrevRefreshRequest(refreshRequest);
setCurrentGetData(getData);
}
useEffect(() => {
currentGetData(someId);
}, [currentGetData, someId]); You’d also need to put Note that in general the pattern of passing down async functions for fetching seems shady to me. I guess in your case you use Redux so that makes sense. But if async function was defined in a parent component, it would be suspicious because you would likely have race conditions. Your effects don’t have a cleanup so how can you know when the user picks a different ID? There’s a risk of requests arriving out of order and setting the wrong state. So that’s just something to keep in mind. If data fetching is in the component itself then you can have an effect cleanup function that sets an “ignore” flag to prevent a setState from the response. (Of course moving data to an external cache is often a better solution — that’s how Suspense will work too.) |
@gaearon the issue I see is that doing it in the event handler means the side effect occurs before the update is committed. There isn’t a strict gauruntee that the component will successfully re-render as a result of that event, so doing it in the event handler can be premature. For example, if I want to log that the user has successfully submitted a new search query and is viewing the results. If something goes wrong and the component throws, I wouldn’t want that log event to have occurred. Then there’s the case where this side effect might be async so using There’s also the problem of In either case, the approach you recommend is likely sufficient. Store the composed state in a form where you can still access the individual values it composes. |
I have a convenience hook for wrapping functions with extra parameters and passing them down. It looks like this: function useBoundCallback(fn, ...bound) {
return useCallback((...args) => fn(...bound, ...args), [fn, ...bound]);
} (it's actually a little more complicated because it has some extra features but the above still shows the relevant problem). The use case is when a component needs to pass a property down to a child, and wants the child to have a way to modify that specific property. For example, consider a list where each item has an edit option. The parent object passes a value to each child, and a callback function to invoke if it wishes to edit the value. The child does not know which item ID it is showing, so the parent must modify the callback to include this parameter. This could be nested to arbitrary depth. A simplified example of this flow is shown here: https://codesandbox.io/s/vvv36834k5 (clicking "Go!" shows a console log which includes the path of components) The problem is that I get 2 linter errors with this rule:
Changing it to use a list of parameters (i.e. not using the spread operator) would destroy the memoisation, because the list is created with each invocation even if the parameters are identical. Thoughts:
|
Hi @gaearon, I just got this warning, which I have not found discussed anywhere:
I find this message a bit confusing. I guess it tries to warns me that the ref current value at cleanup can be different from the value at the effect body. Right? My case, if interesting: CodeSandbox I think I could circumvent this warning by hiding the ref read inside a function, or creating another boolean ref for the cleanup case. But I find unnecesarily verbose if I can just ignore this warning. |
Yeah that sounds like a niche use case. I think when most people want "side effects" they mean form submission itself — not the fact that you viewed a submitted form. In that case the solution I provided seems fine.
Please file a new issue for proposals to change the lint rule.
You can always
File a new issue pls. @CarlosGines
Yes.
Ummm.. not if that leads to a bug. 🙂
Yeah maybe this use case is legit. File a new issue to discuss pls? |
I'm going to lock this issue since we got enough feedback and it has been incorporated. Common questions & answers: #14920 (comment) If you want a deep dive on We'll be adding more stuff to docs soon too. If you want to change something in the rule or aren't sure your case is legit, file a new issue. |
We've posted an RFC that we think provides a better solution for some of these cases. Would appreciate your input: reactjs/rfcs#220 |
Adds user authentication workflow
Common Answers
💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡
We analyzed the comments on this post to provide some guidance: #14920 (comment).
💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡
What is this
This is a new ESLint rule that verifies the list of dependencies for Hooks like
useEffect
and similar, protecting against the stale closure pitfalls. For most cases it has an autofix. We'll add more documentation over the next weeks.Installation
ESLint config:
Simple test case to verify the rule works:
The lint rule complains but my code is fine!
If this new
react-hooks/exhaustive-deps
lint rule fires for you but you think your code is correct, please post in this issue.BEFORE YOU POST A COMMENT
Please include these three things:
But my case is simple, I don't want to include those things!
It might be simple to you — but it’s not at all simple to us. If your comment doesn't include either of them (e.g. no CodeSandbox link), we will hide your comment because it’s very hard to track the discussion otherwise. Thank you for respecting everyone’s time by including them.
The end goal of this thread is to find common scenarios and transform them into better docs and warnings. This can only happen when enough details are available. Drive-by comments with incomplete code snippets significantly drive down the quality of the discussion — to the point that it's not worth it.
The text was updated successfully, but these errors were encountered: