facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.93k stars 46.84k forks source link

Design decision: why do we need the stale closure problem in the first place? #16956

Open slorber opened 5 years ago

slorber commented 5 years ago

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)

image

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):

image

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.

image

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"

image

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:

const useGetter = <S>(value: S): (() => S) => {
  const ref = useRef(value);
  useIsomorphicLayoutEffect(() => {
    ref.current = value;
  });
  return useCallback(() => ref.current, [ref]);
};

(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:

onMount(async () => {
  let isEligible = getIsEligible();
  let data = await fetch(...);
  // at this point, isEligible might has changed: we should rather use `getIsEligible()` again instead of storing a boolean in the closure (might depend on the usecase though, but maybe we can imagine isEligible => isMounted)
  if (isEligible) {
    doStuff(data);
  }
});

As far as I understand, this might be the case:

So you can easily get into the same situation even with a mutable source value. React just makes you always deal with it so that you don't get too far down the road before you have to refactor you code to deal with these cases anyway. I'm really glad how well the React community has dealt with this since the release of hooks because it really sets us up to predictably deal with more complex scenario and for doing more things in the future.

A concrete problem

A react-navigation-hooks user reported that his effect run too much, using the following code:

image

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:

image

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 than useCallback.

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

bvaughn commented 5 years ago

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.

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?

Are we safe using workarounds?

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 useEffectSafe shown in the issue description above uses useEffect. This is okay since it's self contained, but if you were to use useEffect for a generic callback wrapper- it would be even more likely to trigger a bug like the one above- since all useLayoutEffect functions would run before useEffect updated the ref.

I think that implementing a hook like this correctly would require React to add an API like useMutationEffect (see #14336).

Andarist commented 5 years ago

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?

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.

This has a pretty serious flaw if you were to ever pass the callback it creates as a prop to a child component.

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?

bvaughn commented 5 years ago

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)

I'm not sure this is my understanding of the original claim:

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.

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?

Consuming stuff just for internal hook's usage seems OK though, right?

If you're referring to an example like useEffectSafe above, where the ref is only used by another effect within the same hook, I think that seems okay.

I think you can probably accomplish the same thing just using a utility like memoize-one btw (assuming the callback accepts props).

sebmarkbage commented 5 years ago

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 Closures

Let me first point out that (same as in classes), this is not limited to closures. The same thing happens with objects too.

<Foo style={{foo}} />

This captures stale data in the same way as closures do.

<Foo onClick={() => foo} />

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.

Composability

The 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.

useEventCallback

The useEventCallback concept is basically something like this:

useEventCallback = (fn) => {
  let ref = useRef(fn);
  useEffect(() => ref.current = fn);
  return useCallback(() => ref.current.apply(this, arguments));
}

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 Separate

I'd argue that the useEventCallback pattern is actually not necessary in general. It comes from using React state where you could be using refs.

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:

let [state, setState] = useState(false);
let fnA = useCallback(() => {
  setState(true);
}, [state]);
let fnB = useCallback(() => {
  if (!state) doStuff();
}, [state]);

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:

let ref = useRef(false);
let fnA = useCallback(() => {
  ref.current = true;
}, [ref]);
let fnB = useCallback(() => {
  if (!ref.current) doStuff();
});

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, useCallback is sufficient because it won't invalidate as often as if the state was also captured there.

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

Now as lib authors we have to think not only about the inputs/outputs, but also about preserving identities or not

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

the dogmatism of absolutely wanting to conform the ESLint rules

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 useCallback and useMemo because we can infer it. I believe that this will alleviate a lot of the pain points since as a lib author you don't necessarily have to think about adding them, and you can assume that updates are much more fine grained in the system in general.

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.

slorber commented 5 years ago

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.


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?

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 navigate function may change when the boolean goes from false (render1) to true (render2), yet, executing that function produces the same behavior, even if it's navigate1 or navigate2, doing navigate1("nextScreen") or navigate2("nextScreen") would lead to the exact same result. So in such case, even if navigate1 is executed, it does not really look like a problem to me. But I understand it might be tricky and wouldn't recommend this, because we can't really know it's safe without looking at implementation details (ie, does "navigate" capture any variables, ie navigate1 really equivalent to navigate2?).

That's why I'd use useEffectSafe, to ensure that navigate2("nextScreen") gets called on render2. I might as well use the getter pattern and do getNavigate()("nextScreen").

He just wants to operate on "current" props/state (mostly)

@Andarist yes that's what I meant, it looks to be a safer default to always operate on current values.

If you're referring to an example like useEffectSafe above, where the ref is only used by another effect within the same hook, I think that seems okay.

So as I understand it, useEffectSafe is ok because it guarantees the effect executes after the ref mutation, but my getter pattern might not work in all cases (see the example)

I think you can probably accomplish the same thing just using a utility like memoize-one btw (assuming the callback accepts props).

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 useMutationEffect, that could be the job. Wonder about the naming. I'm not already a fan of "useLayoutEffect" which ends up being used for things totally unrelated to layout. As far as I understand, to implement the getter/dynamicCallback safely, we need something quite similar to what the dom provides with events, a top down capture phase, and a bottom-up bubbling phase. What about useCaptureEffect? Just thinking out loud.

Andarist commented 5 years ago

Not Just Closures

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 dispatch from the parent component - but only if the own logic/render is not related to a particular action. Hoisting state is always not a feasible solution because we'd really want to keep the behavior implementations with the component in question, even if it's related to the "state" of a parent.

Keep Imperative Code Separate

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 useEventCallback-like solution. The latter - as showcased here - can break when introducing parent-child relationships which is something to watch out for, unfortunately.

Also a sideways question - is useCallback's cache semantically guaranteed? useMemo's isn't (as stated in the docs) and useCallback is mentioned to be semantically equivalent to useMemo(() => callback) - but it's not obvious from the docs what are guarantees around useCallback's cache

slorber commented 5 years ago

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 closures

Not sure to understand what is stale here, considering foo is not stale in the first place, and it's regular JSX (ie, not returned by a useMemo closure or another fancy patterns)

<Foo style={{foo}} />

It's possible that these patterns can be redesigned to avoid closures.

Do you have an example of what you have in mind?


Composability

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.

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 useMutationEffect. I like how useEffect is consistent to other hooks in this regard and having exceptional behaviors would probably not help.


useEventCallback

useEffect is not always safe because there's a potential gap between commit and useEffect. Many people know that, but what people don't know is that useLayoutEffect also isn't completely safe because sometimes these callbacks are used by other effects deeper in the tree that fire first.

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 ;)

In Concurrent Mode, we try hard to move as much as we can out of this critical "commit" phase.

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?

"Callbacks" in React are used in two ways. Either as a pure function during render or as part of a reducer, or an imperative method used as event handlers with side-effects. E.g. render props. Currently, simple functions and useCallback can be used for either. We'd need to be very clear that these functions cannot safely be called during render and separate the pure ones from the event callback ones. If this was a worth while strategy this could be doable.

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

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.

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

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.

Agree.


Strict Lint Rule

It's really hard to know that it is safe to break it.

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).

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 useCallback and useMemo because we can infer it.

Wow can't wait to see that in action :o

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.

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 ;)

Baloche commented 5 years ago

A little bit off topic but I need some clarification : In every implementation of useDynamicCallback that I see, a useEffect or a custom useIsomorphicLayoutEffect is used, but why ? What is the point of updating the ref inside a use*Effect instead of just doing it outside like the following ?

function useDynamicCallback(callback) {
  const ref = useRef()
  ref.current = callback
  return useCallback((...args) => ref.current.apply(this, args), [])
}
slorber commented 5 years ago

@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.

slorber commented 4 years ago

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

image

To me, it seems like if we had a useMutationEffect / useCaptureEffect that executes from parent to children, before useLayoutEffect, that would:

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 useStateWithGetter, so it seems more and more people are using these patterns to solve the stale closure

slorber commented 4 years ago

Hey, looking at the newly open-sourced Recoil, it seems it's using a pattern a bit similar to the "getter" pattern: useRecoilCallback which permit to get access to current state in a callback. Not sure yet if the returned callback is "stable" though, will have to check.

image

So maybe the following will make sense?

useEffect(
  useRecoilCallback(({getPromise}) => {
    const state = await getPromise(stateAtom);
    console.log('state ', state);
  }),
  [when, i, want, to, reexecute]
)
aantthony commented 4 years ago

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.

markerikson commented 4 years ago

@aantthony note that your example there isn't Concurrent Mode safe, because it's mutating the ref in the middle of rendering.

fabb commented 4 years ago

@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.

bvaughn commented 4 years ago

@fabb Mutating refs during render can cause bugs, like the one I describe here https://github.com/facebook/react/issues/18003#issuecomment-583875570

It would be expected to cause bugs in simple scenarios too though:

dani-mp commented 4 years ago

Wow, I didn’t know this. When is it safe to update a ref then? Just in an event handler?

bvaughn commented 4 years ago

You can update a ref from inside of useEffect or useLayoutEffect. This is probably the most common way to write to them.

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.

Andarist commented 4 years ago

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.

bvaughn commented 4 years ago

My gut tells me what you would often useState (or useReducer) for that sort of use case, but admittedly I haven't built any gesture recognition stuff. @trueadm is kind of our resident expert in that regard.

trueadm commented 4 years ago

@Andarist You should be able to co-ordinate event handlers in effects, rather than in the render phase. That's what we've done internally and also I believe it's how @necolas built React Native Web's responder event system with refs in user-land.

bvaughn commented 4 years ago

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.

trueadm commented 4 years ago

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).

mainfraame commented 3 years ago

@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:

bvaughn commented 3 years ago

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).

slorber commented 3 years ago

Hi,

Just wanted to know if there's any news of a potential useMutationEffect or something similar that runs effects top->bottom?

I've seen the recent discussions between @Andarist and @sebmarkbage here: https://github.com/reactwg/react-18/discussions/110#discussioncomment-1528692 about useInsertionEffect, and wonder if this new effect could help implement useDynamicCallback in a more reliable way (it doesn't look like it's this hook's purpose though).

Curious if there's anything new to avoid the stale closure problems planned for React 18

Andarist commented 3 years ago

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
}
sebmarkbage commented 3 years ago

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.

gaearon commented 2 years ago

We've submitted a proposal to solve this. Would appreciate your input!

https://github.com/reactjs/rfcs/pull/220