React: Question: Hooks + Suspense Interactions. Hooks unstable if suspended.

Created on 10 Jan 2019  Ā·  5Comments  Ā·  Source: facebook/react

Do you want to request a feature or report a bug?
Question about the interaction when using Hooks + Suspense. *I am aware that these are not finalized yet. Please close if this has already been considered.

What is the current behavior?
I tweeted about this:

  1. https://twitter.com/tazsingh/status/1083314900586373120
  2. https://twitter.com/tazsingh/status/1083329402308562944

In essence, Hooks require a full execution of a Component function to register. However, Suspense interrupts that execution such that Hooks aren't able to effectively register. As such, it's impossible to rely on the output of any Hooks until all suspended calls are resolved.

Since the vision for Suspense is to use them as the primary async mechanism in React and since Hooks are meant to be composable items within a Component, where it isn't strictly important to be aware of what is contained in a Hook for it to be useful, I can see this as problematic if they cannot register properly because some other Hook has suspended.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

Please feel free to uncomment the section in this CodeSandbox to observe the behaviour I'm noticing. I also describe a related behaviour in my second tweet regarding usage of useMemo.
https://codesandbox.io/s/9y64nmkrzy

What is the expected behavior?
Frankly, I am unsure of the expected behaviour as I believe both of these features to be working as intended hence why I titled this issue as a "question". I am aware that I am using these features before they are released and proper communication of the usage between Hooks + Suspense is not yet available.

If this is the intended behaviour, cool šŸ‘ If not, I am happy to file a bug report accordingly.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.8.0-alpha.0

Thanks! šŸ™

Most helpful comment

Yeah, when a component Suspends, we don't preserve any of its intermediate state (on either mount or update). We throw it out, and try again once the promise resolves. As Dan mentions, we have some planned optimizations that will allow us to reuse partially completed work ("resuming"), but even then, we would only memoize partial work at the component level, not at the Hook level. (Theoretically we could make it work with Hooks but that's not in our current plans.) Still, this would only be an optimization. It's not something you would be able to rely on. There's a section in this RFC that discusses our rationale a bit: https://github.com/acdlite/rfcs/blob/context-write/text/0000-context-write.md#caching-external-data

computeExpensive will be called n times until all Suspense calls are resolved.

One option is to cache the result of componentExpensive in an external cache. You could even offload it to a worker and suspend. The benefit of this approach is the computation can be reused across multiple components.

All 5 comments

What is your expected and actual output?

It’s not exactly clear to me what you’re trying to do and what the problem is.

(In general, suspending aborts render and it shouldn’t matter for Hooks because they’ll run next time again.)

Perhaps this contrived example illustrates my question a bit better? https://codesandbox.io/s/x36o43mw
You'll note that computeExpensive is called n+1 times with Suspense but once without it.

I understand what you're saying regarding aborting the render and it makes complete sense.
The nature of my question revolves around more complex components where anything can suspend and invoke the Component function n times, thus computeExpensive will be called n times until all Suspense calls are resolved.

Expected/hopeful behaviour
computeExpensive is called once.

Actual behaviour
computeExpensive is called n+1 times, where n is the number of times the Component function is executed before all suspenders have resolved.

In this contrived example, I can pull it out very easily. However, for a real app, it may be problematic when something that suspends (potentially a 3rd party thing) is dropped into a component and now previous memos are being executed multiple times.

Sounds like you potentially answered my question in that this is by design and proper communication of this behaviour will be released as the feature is officially released?

Thanks again @gaearon!

I’m on mobile now but let’s see if this clarifies things. Note I might be wrong so check this against your observations.

If component suspends during mount, I think we don’t preserve its state on retry. We attempt to mount from scratch again instead. So it’s expected memo or state aren’t preserved. Suspense is intended to be used with caches outside a component rather than backed by local state. (useMemo is a fair point that isn’t addressed by that though.)

If I’m correct, in the future we plan to implement ā€œresumingā€ which is an optimization and should allow to reuse results from suspended mount on next attempt. I think maybe this could help the useMemo issue. But I don’t think it would provide strong guarantees either.

I’ll ask @acdlite if this writeup is correct.

Yeah, when a component Suspends, we don't preserve any of its intermediate state (on either mount or update). We throw it out, and try again once the promise resolves. As Dan mentions, we have some planned optimizations that will allow us to reuse partially completed work ("resuming"), but even then, we would only memoize partial work at the component level, not at the Hook level. (Theoretically we could make it work with Hooks but that's not in our current plans.) Still, this would only be an optimization. It's not something you would be able to rely on. There's a section in this RFC that discusses our rationale a bit: https://github.com/acdlite/rfcs/blob/context-write/text/0000-context-write.md#caching-external-data

computeExpensive will be called n times until all Suspense calls are resolved.

One option is to cache the result of componentExpensive in an external cache. You could even offload it to a worker and suspend. The benefit of this approach is the computation can be reused across multiple components.

Amazing. Thanks for your assistance @acdlite & @gaearon! Much appreciated.

Since it sounds like this is working as designed and is therefore not an issue, I'm going to go ahead and close this.

I'd like to discuss this behaviour further and shall take that discussion to the RFC šŸ‘

P.S. Grabbing you both a drink the next time I see you šŸ˜‰ Really enjoying playing with these new features!

Was this page helpful?
0 / 5 - 0 ratings