-
Notifications
You must be signed in to change notification settings - Fork 47.5k
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
[Feature Request]: distinguish "what" and "when" dependencies in useEffect #19820
Comments
Am I missing something, or |
@vkurchatkin I should do a better job at describing the proposal. |
What does it mean to "recompute" a callback? |
An effect callback is just a JavaScript function that closes over variables in scope.
|
A change to built-in hooks APIs is really the sort of proposal that should go through our RFC process: |
You could build this hook in user space for what it's worth. At least if I'm understanding you correctly, something like?
|
What's the point of recomputing the callback if you are not going to run it? |
I think the main thing being asked for here (if I'm understanding) has nothing to do with "recomputing" really. (I think that's a confusing term to use to refer to a function.) I think the real request is to only run a callback when certain, specific dependencies change. |
But that's what useEffect already does |
Yes and no. It invokes a callback any time any dependency changes. I think @jsamr is saying that he doesn't want the callback to be re-run unless a subset of the dependencies change. |
I find the proposed API a little confusing. (I think it's easier to reason about a single dependencies array.) But I have heard the question (how to implement this) come up a few times in the past. |
Then you only need to list this subset as dependencies. |
No, that's not true. You have to specify everything you reference inside of the effect as a dependency– or your callback may close over stale values. |
@bvaughn There must be a better wording for this. For example:
Each time
Yes, I'm well aware! But I wanted some help with the phrasing which obviously was required, and basic feedback on the feature itself.
Absolutely. Any dependency that is not referenced in the body of the callback is, per my definitions, a "whenDep". |
I don't think that's true, necessarily. I think more commonly the "when" deps would be referenced too, no? |
Well, that's just what linter wants) In practice you list dependencies that trigger effect. Most of the time they match all used values, but sometimes you have to omit something. |
A implies B doesn't mean B implies A! So yes, often the "when" deps and "what" deps coincide. |
Well, I disagree here (I guess I'm "I know what I am doing" kind of person). But, most importantly, this proposed change doesn't actually work around this problem in any way. It just disables linter by adding an unchecked set of dependencies. |
@jsamr I think that's what you want:
Not sure what's the use case for this is |
The lint rule is very important. Omitting values might currently work the way you want– in that they can change without your function being called. That being said, I think there are two problems with this:
|
@bvaughn Thank you for the implementation you proposed!
I believe this is not an accurate assumption. If you look at the use case I provided, it depicts a situation where I don't want the scroll effect to be triggered on padding changes, only when content changes. This is very UX-specific. |
I don't think the use of
It's not so much an assumption as it is based on personal experience from talking about use cases and questions like this. 😄 I didn't say all cases, but I do think many of them. |
The problem with the lint rule is that it works only in subset of cases. Specifically, it implies that effect is idempotent and it "synchronises" some values with some external state, so it's ok and actually good to run as often as possible |
I agree, but it just seems what @jsamr is describing. If you remove it you'll just get plain old |
Which I already responded to above. I don't think adding an extra |
Have to step away from GitHub for a while now. I suggest moving this conversation to an RFC and closing this issue though! |
@bvaughn I agree, thanks for your invaluable feedback! Do you recommend that I read some relevant references prior to writing the RFC? |
@jsamr You can get the desired behavior like this:
In my opinion this communicates clearly that, when content changes, you want to set the scroll to the current value of For the record: I would vote against your feature request. I think it adds unneeded complexity to the API for a situation that I have seen very rarely in real world applications. |
@EECOLOR |
That is incorrect, specifically this line:
You are not allowed to mutate refs during render |
@vkramskikh Well I tried experimentally his solution and it worked. My understanding is that a ref will always be the same throughout all render cycles (hence its name...), so you can do whatever you want with its own attributes; but there might be implications that I am not aware of. Could you elaborate on why this is allegedly "not allowed", or provide a reference in the docs or expert blog? I am surprised that the rules of hooks eslint plugin wouldn't catch this one, if indeed it is considered by React core contributors misuse of the hook API. |
https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
The language is pretty mild though. It's not a recommendation, more of fundamental rule of React: no side effects in render. Mutating a ref is a side-effect like any other.
Looks like there is a PR for this: #19095 |
@vkramskikh Thanks for this fair point, I'll reopen. |
@EECOLOR Given Vladimir insight, I am going to disagree here. The implementation (reworked from @bvaughn proposition) is 8 lines of code:
The drawback of a user land implementation is that the linter rules of hooks won't be able to properly track the dependencies.
Certainly, I can't speak to that, statistic wise, and you have obviously an order of magnitude more experience than I have. However, even if it takes 1 or 5% of use-cases, I believe it is worth having an official, documented way to do it, supported by the linter rules. I personally find the ref comparison solution very cumbersome. |
Linter rule can't track this anyway. Linter rule will be able to check Also, this:
is doing exactly the same as this:
So essentially the only thing you get out of this is disabling linter rule, which you can already do |
@vkurchatkin Well, this is not true, because "whatDeps" might have changed, and thus cause the callback to update with new values, whereas in your example, "callback" might have outdated variables in its scope (the famous stale closures @bvaughn was referring to). |
No, the callback always has the values captured during last render when it is triggered. |
@vkramskikh What you are saying was actually my intuitive approach to the problem until I met the rules of hooks. I could reproduce your claim here, but it is no proof your claim will always be true. @bvaughn previously stated:
Given that he is a core React contributor, and with all respect and regard for your willingness to help, I would rather put my trust in his statements! |
That's the problem. You can't (and shouldn't) solve linter problems with API
This sounds reasonable. The problem is that he talks about a different thing. You can't "capture" variables in an effect without running it. Anyway, can you provide an example where the first snippet and the second snippet produce different results? |
@vkurchatkin First of all: thank you for pointing this out. It however is more subtle than you describe.
This is not actually true (the part about it not being allowed, it is true that mutating a ref is a side effect). Check for example: https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops. Here the side effect is The ESLint rules pull request points to this comment which talks about a specific use case where this could cause a problem. Here the returned I'm not sure if in the use case of |
@vkramskikh If what you say is true, then I totally agree that the linter should be fixed instead.
I would very much like to do so. But I am not familiar with React codebase, and thus I won't be able to guess the conditions for the stale closure inconsistency to occur. @bvaughn Could you or any core contributor help us understand the detailed reasons why Vladimir's solution could lead to stale closures, and eventually provide an example where this is happening? One could start with the example I provided. EDIT Update example |
It is true in general. There are a couple of exceptions, specifically calling
Of course it could be. It is exactly same thing. In concurrent mode there is no clear concept which render is last or whether it's even going to be used.
Stale closure problem is not related to React internals really. Here is an example:
The only solution to this issue is to rerun the effect:
|
@vkurchatkin Can you describe a situation where this example would have unexpected results caused by concurrent mode?
|
I might have misunderstood your point but, if that were true, it would be very surprising that Dan Abramov himself used the expression "the stale closure pitfalls" when introducing exhaustive deps lint rule! |
@jsamr I think @vkurchatkin means that this is a property of javascript:
You could say it is related to React internals, but only because React handles a certain behavior for us. So if we use something else for that same behavior, we get the same problem. I would conclude that you are both right, depending on how you look at the situation. |
Catching up on this discussion. 😄
My comment was more of a broad statement that– if you omit dependencies from any of the deps arrays (e.g. In the scope of this specific discussion, the effect wouldn't have stale values when run (since it would close over the most recent values at the time it runs) but I still think the idea is not a good one for reasons I mentioned above.
The official recommended way is to use a ref to track the subset of values/changes you care about. It looks like this issue has settled enough now that I'm going to close it and suggest that the discussion/proposal gets summarized as an RFC: Thanks everyone! |
I should probably note this proposal was initially filed in the RFCs repo (but as an issue). I suggested re-filing here because it didn't seem like it was ready for an RFC, and I was hoping to find time to respond to why I don't think it works in more detail here. An RFC is fine if the author feels ready to write it but I just want to make sure we don't create an impression that we're avoiding the discussion by kicking the issue between two repositories. |
Thanks for adding that context, Dan. I had no idea it had been mentioned in the RFC repo initially. Didn't see a cross link or anything. I imagine there's probably enough info now, after the above discussion, for an RFC to be written though 😄 |
I also came to think of an other way to define an API to tackle the underlying issue of "executing the useEffect callback on a subset of dependencies changes", which would imply using named dependencies, and is similar to
|
@jsamr once again, doesn't solve anything: if |
@jsamr I think it'd be better to move the reasonning down to an alternative In all cases, the solution must take into account that The issue is that callabck dependencies play a double rule:
In some case we want both 1 and 2 (like with render callbacks passed to a child Component). In other cases we want only 1: we just like to update the values seen by the callback. This is typically the case for callbacks that aren't invoked by React but some external notifier like DOM event handlers (unless the handler's code itself changes). React doens't control when those callbacks will be invoked, so it makes sense to always access latest committed state in this case. The solution usually is to close over a mutable ref (there's an ambiguity for And finally a third (and the most subtle) kind is the A compiler can probably distinguish between callbacks scheduled by React and those invoked by the external world, but the challenge is
A manual implementation could be done in user space as mentionned by @bvaughn but the lint rule seems unable to verify deps that aren't passed as an array literal. |
Feature
A new overloading for
useEffect
(Typescript syntax):Motivations
In the current implementation, the second argument of
useEffect
, “deps”, is described as such:This definition does not account for an important nuance between two kind of dependencies:
recomputedupdated;rerunexecuted.The community seems to be in need of a solution, see https://stackoverflow.com/q/55724642/2779871.
Use case
I want to scroll to top of a component when the content changes (first dependency), but this effect also depends on a variable padding top (second dependency).
With the current implementation of
useEffect
, this is what I would do:There is an undesired behavior: the hook is executed on
paddingTop
changes. Moreover,content
is not, semantically, a dependency of the callback, but rather a dependency of when this side effect should take place. So I could use a ref, store the previous value ofpaddingTop
, and compare the two. But that is cumbersome.What I would like to do, is express the when this side-effect should take place dependencies declaratively:
Detailed behavior
My understanding is that this proposal would not be a breaking change and is 100% retrocompatible with current implementation.
One argument
The behavior is identical to current implementation. The effect is executed after each render cycle.
Two arguments
The behavior is identical to current implementation. The second argument conflates whatDeps and whenDeps.
Empty second argument
The behavior is identical to current implementation. The callback is executed only once.
Empty third argument
The callback is executed only once, regardless of the changes in whatDeps.
Three arguments
The callback is executed when and only when at least one variable in whenDeps array changes, regardless of the changes in whatDeps.
The text was updated successfully, but these errors were encountered: