-
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
Design decision: why do we need the stale closure problem in the first place? #16956
Comments
I'm a little skeptical of this. It seems like you're saying that it's okay for certain values that your function operates on to be totally arbitrary. Can you provide a couple of concrete cases when this is true?
This is a really long GH issue, but I wanted to respond to this particular question. There is a potential serious bug when it come to using effects and refs to wrap closures, given React's current hooks primitives. First, here's the similar pattern I've seen used (within Facebook too): function useDynamicCallback(callback) {
const ref = useRef(callback);
useIsomorphicLayoutEffect(() => {
ref.current = callback;
}, [callback]);
return useCallback((...args) => ref.current(...args), []);
} This has a pretty serious flaw if you were to ever pass the callback it creates as a prop to a child component. For example: function Example(props) {
const callback = useDynamicCallback(props.callback);
return <Child callback={callback} />;
}
function Child({ callback }) {
useLayoutEffect(() => {
// Child effects are run before parent effects!
// This means the callback ref has not yet been updated-
// which means that it still points to the previous version,
// with stale values and potentially different behavior.
// This function call will probably cause problems!
callback();
}, []);
} Note that I think that implementing a hook like this correctly would require React to add an API like |
From what I understand @slorber's intention - it's not about operating on arbitrary values. He just wants to operate on "current" props/state (mostly), but this with hooks leads to a memoization dance (especially when developing libraries where we don't want to assume a position in component hierarchy etc so it seems safer to provide/accept memoized callbacks. So this is just a discussion about recommended patterns and how we could possibly try to avoid this problem.
Thanks for a compelling example! Seems like indeed providing a "memoized" callback with this technique is currently a no-go. Consuming stuff just for internal hook's usage seems OK though, right? |
I'm not sure this is my understanding of the original claim:
If you don't want to re-capture when values change, then the values you'll have closed over won't be "current". But maybe some concrete examples would help clarify if there's a misunderstanding?
If you're referring to an example like I think you can probably accomplish the same thing just using a utility like |
There's a bit to unpack here and your original comment doesn't quite line up with how I see this progression so I'll respond in a different format so pardon me if I don't answer some questions directly. This is very fair feedback and a general pain point people have right now. In fact, this is basically Vue's whole point with their version of Hooks to support mutable containers being captured by closures. It's a valid approach but it does cause a lot of downstream effects. So we're hoping we can achieve a different approach over time. Not Just ClosuresLet me first point out that (same as in classes), this is not limited to closures. The same thing happens with objects too.
This captures stale data in the same way as closures do.
The difference is that closures might be a bit more opaque. You might be able to do a shallow comparison on an unknown object but it happens there too. It just happens that there are common patterns that happen to use closures as the most idiomatic API. It's possible that these patterns can be redesigned to avoid closures. ComposabilityThe most common way this happens is by passing something through props. This creates a copy of the value. This decoupling is how we are able to create many composable abstractions. It is very common and idiomatic in React so we can't dismiss that this is going to be the most common form. This has indirect downstream effects because if you can't rely on mutation propagating in one intermediate level, you can't rely on it deeper neither. The primary goal of React's API is to define an API boundary between abstractions. A common language that has certain properties that you can rely on. That's why it's important that it's clear when certain values have certain properties. E.g. refs provide a common language to talk about a "box" that may contain mutable values for interop with imperative systems. If a Hook isn't responsive to new values, that break such a contract. So in general, it would be a bad idea to let that pattern spread. useEventCallbackThe useEventCallback concept is basically something like this:
I actually came up with this very early in the process, before Hooks was a thing. However it has some bad qualities that lead me to want to strongly discourage this pattern.
If this was the only way to solve things, then we should adopt this upstream but I don't think this is ever actually the best solution available. So let's see what else we can do. Keep Imperative Code SeparateI'd argue that the If you get into this situation it's not because you have a nice purely functional model. It's because you have interop with imperative code and that code likely relies on subtle mutation anyway so it's not suitable to mix with React's deferred/batched/concurrent state. E.g. imagine if your callback was using state like this:
This is probably not great anyway since the setState can be batched and deferred so it might not update the second callback immediately. If this instead used a ref then it wouldn't get invalidated all the time:
Using a ref and imperative code is discouraged if you can avoid it, but if you're writing imperative code anyway then this might be the best way to solve it. In this code, I think the issue is that React doesn't really make it easy to tell when you're entering imperative code and should be using a ref vs not. Also, it's not easy to read a ref in a render function easily. We have some work to do there. Referential Identity == Best Practice
It seems to me that this is a natural evolving best practice just like other performance considerations. In libraries that work on the mutation model, you also have to think about if you're returning a reactive value that has its origin traced in an efficient way or if it's a derived copy that will be reevaluated. I think we can do a lot more to make this automatic, but not if you're using patterns that aren't optimizable by us. For example, the useRef + useCallback pattern above is describing semantics that are easily optimizable in our current approach. Similarly passing down a stablee dispatcher instead of a callback that closes over state is best-practice that helps avoid the perf problems to begin with. Strict Lint Rule
I think this is actually quite important. We constantly get questions about why something didn't work and it's almost always because someone decided to break the rules in one place and the consequences snowballing elsewhere and then it's too late to refactor the code. It's really hard to know that it is safe to break it. However, the strictness also is about promoting patterns that are automatable. The lint rule is just the first step. The next step we added was "autofixing" that added dependencies for you. The next step is adding a compiler that adds all the memoization for you in the entire app. That way you don't have to think as much about However, this can only automate what you're already expressing today. So if today's code isn't working then we need to either change our patterns, or shift the approach used by React. Breaking the rules, isn't just a convenience, it means that automation isn't safe. |
Thanks @bvaughn for your answer. I'm happy you come up with this example because I felt there was something dangerous with my pattern but couldn't come up with a concrete example to illustrate that. I was mostly thinking about the potential execution order of hooks inside the same component, but not about execution order for parent/children relationships.
Not really sure what's the best way to express this, but what I mean is sometimes, if the closure captures a variable, we don't really care if this variable was the render1 variable or render2 variable, even if the identity changes, because that variable may be "semantically equivalent" even if the identity is different. const Screen = () => {
const { navigate } = useNavigation();
const [someBoolean, setSomeBoolean] = useState(false);
useEffect(() => {
if (someBoolean) {
navigate(...)
}
}, [someBoolean])
} Here, taking react-navigation as an example, the That's why I'd use
@Andarist yes that's what I meant, it looks to be a safer default to always operate on current values.
So as I understand it,
Not sure what you mean here. If navigate1 and navigate2 are safe to invert, I could use useConstant and store navigate1 and always use navigate1 for all renders Thanks again for the example. Didn't know about the |
This is, of course, true - but not that compelling argument (for me). Personally I don't mind implementing some fine-grained memoization where it is needed - it's rather easy to spot where you need it and you don't need all the time. The main problem is with callbacks which always suffer from this problem and you kinda need to memoize them constantly. The problematic pattern mentioned here seems to be the getter pattern which comes really useful to use. I often find myself wanting to read a value somewhere down the tree, but don't want to keep that value in the state as it's not render-related - so this is mostly for being able to read it in things like event handlers. Arguably sometimes such use cases could be refactored to use
This is a summary - could be probably included in the docs somewhere? This is also a pattern I had in mind when mentioning proposing this at code reviews etc with a combination of Also a sideways question - is |
Thanks for your answer and accepting by feedback @sebmarkbage , understand you see a different progression for your answer. It's also hard for me to formulate what I feel is wrong, the issue may look a bit messy :) Not just closuresNot sure to understand what is stale here, considering <Foo style={{foo}} />
Do you have an example of what you have in mind? Composability
That seems legit. That's why I'm not sure the current API should be modified either, but still looking for a solution: that could be just a safe, recommended and widespread pattern to use, or maybe the useEventCallback
Thanks, @bvaughn illustrated that well and that's why I wanted to ask the question here: because I wasn't sure and couldn't find anybody to tell me ;)
The way I think about it is assigning a new value to a ref is fast enough to happen during this phase. But maybe it's more expensive than I think to generalize, and this is not what you want us to do?
Not totally sure to understand what you mean. The following pattern has already bitten me in the past, because of the child not re-rendering due to memo, when the parent count value changes. function Example(props) {
const [count, setCount]
const getCount = useDynamicCallback(() => count);
return <>
<Child getCount={getCount} />;
<Button onClick={() => setCount(c => c+1)}>Increment</Button>
</>
}
React.memo(function Child({ getCount }) {
return <div>{getCount()}</div>
}) I think it's already an antipattern to let data flow down the tree with getters instead of regular props, not sure it's totally related though to what you have in mind though... Keep Imperative Code Separate
Agree, and people probably try intuitively to fit their imperative stuff using declarative constructs. About React-navigation we discussed to expose directly a ref through the API: const Screen = () => {
const navigationRef = useNavigationRef();
const [someBoolean, setSomeBoolean] = useState(false);
useEffect(() => {
if (someBoolean) {
navigationRef.current.navigate(...)
}
}, [navigationRef,someBoolean])
} That would definitively work, but I have never seen such hooks API so far. Is it something you'd recommend? Referential Identity == Best Practice
Agree. Strict Lint Rule
That's true. The thing that bothers me is that sometimes, the code is working by breaking the rule, and fixing the rule might break the desired behavior (it's always possible to rewrite in a way that works and conforms to the rule, but still an important fact to consider).
Wow can't wait to see that in action :o
It's not that it's not working, it's mostly that there are multiple ways to design an API and it's hard to see which design is the good choice. For example in my react-navigation example: function ReactComponent() {
const { navigate } = useNavigation();
useEffect(() => {
if (someBoolean) {
navigate(...)
}
}, [someBoolean,navigate])
} It's not clear if it's my responsibility to make the navigate function stable and how. I would say yes, because it would simplify usage, but it involves some lib boilerplate. What I understand is that in the future this boilerplate could be automated? In the meantime, I'd really like to know how you'd address this specific problem, between all the possible solutions. Having some input/output examples of such a compiler would probably be very helpful to see what kind of code you would expect us to write manually until the compiler is ready. Thanks again for your answer, really appreciate you took the time to answer this in depth ;) |
A little bit off topic but I need some clarification : In every implementation of function useDynamicCallback(callback) {
const ref = useRef()
ref.current = callback
return useCallback((...args) => ref.current.apply(this, args), [])
} |
@Baloche this is a good question and also thought about it. I suspect we don't want to update the ref too early. A dom event triggering this callback might be called in between a render call, and the dom commit of that render call. This could perhaps lead to the wrong callback being executed. For consistency, this seems to be more reliable to update in useLayoutEffect so that your new callback could only be used if the render updating it was actually applied to the dom. |
Hi @sebmarkbage About the "zombie children" problem that react-redux has to deal with (cc @markerikson @dai-shi). A nice explanation is here: https://kaihao.dev/posts/Stale-props-and-zombie-children-in-Redux To me, it seems like if we had a
I think it would be a nice addition to the hooks API. Also note I've encountered other people as well that told me they were using custom hooks like |
Hey, looking at the newly open-sourced Recoil, it seems it's using a pattern a bit similar to the "getter" pattern: So maybe the following will make sense? useEffect(
useRecoilCallback(({getPromise}) => {
const state = await getPromise(stateAtom);
console.log('state ', state);
}),
[when, i, want, to, reexecute]
) |
If anyone finds it useful, I've been using this, and it seems to be working well: export default function useCallback(fn) {
const ref = React.useRef(fn);
ref.current = fn;
return React.useMemo(() => (...args) => ref.current.apply(null, args), []);
} I put it on NPM at https://www.npmjs.com/package/use-callback I had issues with attaching the handler during useLayoutEffect, because callbacks were being executed between render but before the useLayoutEffect. |
@aantthony note that your example there isn't Concurrent Mode safe, because it's mutating the ref in the middle of rendering. |
@markerikson that‘s interesting, could you explain a bit more why this is a problem in concurrent mode? We mutate refs in our components too, and I would like to make it concurrent mode ready. |
@fabb Mutating refs during render can cause bugs, like the one I describe here #18003 (comment) It would be expected to cause bugs in simple scenarios too though:
|
Wow, I didn’t know this. When is it safe to update a ref then? Just in an event handler? |
You can update a ref from inside of You can also use them for lazy initialization but be careful not to expand this pattern to an update. It's only safe to do if you lazily initialize a value- and even then, only if that value does not read from some other mutable source. It's important that it's idempotent. Can you elaborate on the use case of updating a ref from within an event handler? I guess you could do that but I'm not sure I understand why you would. |
For example, implementing gestures - coordinating multiple event listeners using refs. |
My gut tells me what you would often |
I don't think he was talking about coordinating handlers during the render phase, @trueadm. He was specifically talking about writing to a ref from within an event handler. |
Ah my bad! In terms of writing to a ref in an event handler, that's perfectly fine to do in this case IMO. That's core to how we've built some our internal event systems at Facebook; for where you have some state that is not related to the render flow, but rather related to the co-ordination of events through event propagation or through streams (multiple event normalization/sequences). |
@bvaughn I’ve just spent a month figuring out why ag-grid was leaking memory. Background: I found some code was passing api references to useCallback via deps. The apis are mutable, circular and attached as a property to the div element (mounted in their component). That same component calls “destroy” on that api when it is unmounted, but i don’t see any direct code removing that api reference from the DOM element. I originally called a setter on a context provider to set references to their api (for sharing across components), along with commonly used utility methods for stuff like resetting scroll, etc. Assumptions (Leak Culprits):
Solution?
Questions:
|
Sorry @mainfraame. I don't think I have enough context to answer these questions without spending a good bit of time digging into ag-grid to figure out what you're talking about (and I don't have time to do that at the moment). |
Hi, Just wanted to know if there's any news of a potential I've seen the recent discussions between @Andarist and @sebmarkbage here: reactwg/react-18#110 (comment) about Curious if there's anything new to avoid the stale closure problems planned for React 18 |
Hah, when I was writing this comment of mine I was thinking about this issue here and you, Sébastien 😜 According to my understanding - if the order would be flipped to preserve the current injection order of CSS-in-JS libraries then this would solve the problem outlined in the issue here: function useLatestValueRef(value) {
const ref = React.useRef(value)
React.useInsertionEffect(() => (ref.current = value))
return ref
} |
This ones is mostly intended for DOM mutations. The other use case we had in mind for useInsertionEffect is to insert Portal wrapper nodes into a global place. It's nicer if the timing of that is when the children of that portal is already done. We tend to insert at the root once all the children are done. Similarly, DOM nodes are updated after their children so that things like selectedValue works. |
We've submitted a proposal to solve this. Would appreciate your input! |
Hi,
I initially asked this on Twitter and @gaearon suggested me to open an issue instead.
The original thread is here: https://twitter.com/sebastienlorber/status/1178328607376232449?s=19
More easy to read here: https://threadreaderapp.com/thread/1178328607376232449.html
But will try to make this issue more clear and structured about my args and questions.
Don't get me wrong, I really like hooks, but wonder if we can't have smarter abstractions and official patterns that make dealing with them more easy for authors and consumers.
Workaround for the stale closure
After using hooks for a while, and being familiar with the stale closure problem, I don't really understand why we need to handle closure dependencies, instead of just doing something like the following code, which always executes latest provided closure (capturing fresh variables)
Coupling the dependencies of the closure and the conditions to trigger effect re-execution does not make much sense to me. For me it's perfectly valid to want to capture some variables in the closure, yet when those variables change we don't necessarily want to re-execute.
There are many cases where people are using refs to "stabilize" some value that should not trigger re-execution, or to access fresh values in closures.
Examples in major libs includes:
Also @Andarist (who maintains a few important React libs for a while):
We often find in such codebase the "useIsomorphicLayoutEffect" hook which permits to ensure that the ref is set the earliest, and try to avoid the useLayoutEffect warning (see https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85). What we are doing here seems unrelated to layout and makes me a bit uncomfortable btw.
Do we need an ESLint rule?
The ESLint rule looks to me only useful to avoid the stale closure problem. Without the stale closure problem (which the trick above solves), you can just focus on crafting the array/conditions for effect re-execution and don't need ESLint for that.
Also this would make it easier to wrap useEffect in userland without the fear to exposing users to stale closure problem, because eslint plugin won't notice missing dependencies for custom hooks.
Here's some code for react-navigation (alpha/v5). To me this is weird to have to ask the user to "useCallback" just to stabilize the closure of useFocusEffect, just to ensure the effect only runs on messageId change.
Not sure to understand why we can't simply use the following instead. For which I don't see the point of using any ESLint rule. I just want the effect to run on messageId change, this is explicit enough for me and there's no "trap"
I've heard that the React team recommends rather the later, asking the user to useCallback, instead of building custom hooks taking a dependency array, why exactly? Also heard that the ESLint plugin now was able to detect missing deps in a custom hook, if you add the hook name to ESLint conf. Not, sure what to think we are supposed to do in the end.
Are we safe using workarounds?
It's still a bit hard for me to be sure which kind of code is "safe" regarding React's upcoming features, particularly Concurrent Mode.
If I use the
useEffectSafe
above or something equivalent relying on refs, I am safe and future proof?If this is safe, and makes my life easier, why do I have to build this abstraction myself?
Wouldn't it make sense to make this kind of pattern more "official" / documented?
I keep adding this kind of code to every project I work with:
(including important community projects like react-navigation-hooks)
Is it a strategy to teach users?
Is it a choice of the React team to not ship safer abstractions officially and make sure the users hit the closure problem early and get familiar with it?
Because anyway, even when using getters, we still can't prevent the user to capture some value. This has been documented by @sebmarkbage here with async code, even with a getter, we can't prevent the user to do things like:
As far as I understand, this might be the case:
A concrete problem
A react-navigation-hooks user reported that his effect run too much, using the following code:
In practice, this is because react-navigation core does not provide stable
navigate
function, and thus the hooks too. The core does not necessarily want to "stabilize" the navigate function and guarantee that contract in its API.It's not clear to me what should I do, between officially stabilizing the
navigate
function in the hooks project (relying on core, so core can still return distinct navigate functions), or if I should ask the user to stabilize the function himself in userland, leading to pain and boilerplate for many users trying to use the API.I don't understand why you can't simply dissociate the closure dependencies to the effect's triggering, and simply omitting the
navigate
function here:What bothers me is that somehow as hooks lib authors we now have to think about whether what we return to the user is stable or not, ie safe to use in an effect dependency array without unwanted effect re-executions.
Returning a stable value in v1 and unstable in v2 is a breaking change that might break users apps in nasty ways, and we have to document this too in our api doc, or ask the user to not trust us, and do the memoization work themselves, which is quite error prone and verbose. Now as lib authors we have to think not only about the inputs/outputs, but also about preserving identities or not (it's probably not a new problem, because we already need to in userland for optimisations anyway).
Asking users to do this memoization themselves is error prone and verbose. And intuitively some people will maybe want to
useMemo
(just because of the naming) which actually can tricks them by not offering the same guarantees thanuseCallback
.A tradeoff between different usecases in the name of a consistent API?
@satya164 also mentionned that there are also usecases where the ESLint plugin saved him more than once because he forgot some dependency, and for him, it's more easy to fix an effect re-executing too much than to find out about some cached value not updating.
I see how the ESLint plugin is really handy for usecases such as building a stable object to optimize renders or provide a stable context value.
But for useEffect, when capturing functions, sometimes executing 2 functions with distinct identities actually lead to the same result. Having to add those functions to dependencies is quite annoying in such case.
But I totally understand we want to guarantee some kind of consistency across all hooks API.
Conclusion
I try to understand some of the tradeoffs being made in the API. Not sure to understand yet the whole picture, and I'm probably not alone.
@gaearon said to open an issue with a comment:
It's more nuanced
. I'm here to discuss all the nuances if possible :)What particularly bothers me currently is not necessarily the existing API. It's rather:
Those are the solutions that I think of. As I said I may miss something important and may change my opinions according to the answers.
As an author of a few React libs, I feel a bit frustrated to not be 100% sure what kind of API contract I should offer to my lib's users. I'm also not sure about the hooks patterns I can recommend or not. I plan to open-source something soon but don't even know if that's a good idea, and if it goes in the direction the React team want to go with hooks.
Thanks
The text was updated successfully, but these errors were encountered: