React: useTransition does not wait for loading within a new Suspense

Created on 19 Nov 2019  Â·  8Comments  Â·  Source: facebook/react

Do you want to request a feature or report a bug?

Bug or clarification.

What is the current behavior?

When useTransition renders a new Suspense component which wrap the pending resource, the UI is not suspended.

Reproduction: https://codesandbox.io/s/concurrent-react-nov-hfqee?fontsize=14&hidenavigation=1&theme=dark

In the provided codesandbox, the first two times you click the Next button the UI is suspended as expected.

When you click for the third time, the UI no longer suspended and we can see the "Loading Page 3" text. My introspection of why this is happening is because a new Suspense component is rendered, causing the behavior.

What is the expected behavior?

useTransition should always suspend UI regardless if the new changes is wrapped in another Suspense.

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

experimental, specifically 0.0.0-experimental-b53ea6ca0.

Suspense Needs Investigation

Most helpful comment

Yeah it’s expected behavior. It’s explained here: https://reactjs.org/docs/concurrent-mode-patterns.html#wrap-lazy-features-in-suspense

The reasoning is that you don’t want someone adding a slow loading component deep down to regress on every transition to that page. That breaks the local knowledge guarantees of components, forcing everyone to be aware of the whole system. It actually did work this way previously, and we found that it’s very difficult for product engineers in practice.

So the way you can think of useTransition is it only delays it for as long as needed to prevent a bad loading state. And by bad, we essentially mean “hiding existing content”. That’s why it waits for existing Suspense boundaries. But it doesn’t wait for new boundaries because we want to show the other content (outside of them) as soon as possible.

There are some cases when this is insufficient. Maybe some “new” loading state is also “bad” and you really want to avoid it. This is undocumented but passing unstable_avoidThisFallback={true} to Suspense marks it as undesirable even when newly mounted. In that case useTransition will prefer to stay on the previous page for longer than to show it.

All 8 comments

Seeing "Loading Page 3" means that the component is suspended as expected, as you specified here:

<React.Suspense fallback="Loading Page 3 ">
            <DadJoke id={id} />
</React.Suspense>

I don't see any bug here, correct if I'm wrong.

@amazzalel-habib what i expect is seeing the Loading... text on the button like the first two clicks.

Here's a minimal reproduction of the behavior. I believe it's expected that a newly mounted Suspense boundary renders the fallback even if the update was inside a transition, but maybe @acdlite or @gaearon can confirm.

Yeah it’s expected behavior. It’s explained here: https://reactjs.org/docs/concurrent-mode-patterns.html#wrap-lazy-features-in-suspense

The reasoning is that you don’t want someone adding a slow loading component deep down to regress on every transition to that page. That breaks the local knowledge guarantees of components, forcing everyone to be aware of the whole system. It actually did work this way previously, and we found that it’s very difficult for product engineers in practice.

So the way you can think of useTransition is it only delays it for as long as needed to prevent a bad loading state. And by bad, we essentially mean “hiding existing content”. That’s why it waits for existing Suspense boundaries. But it doesn’t wait for new boundaries because we want to show the other content (outside of them) as soon as possible.

There are some cases when this is insufficient. Maybe some “new” loading state is also “bad” and you really want to avoid it. This is undocumented but passing unstable_avoidThisFallback={true} to Suspense marks it as undesirable even when newly mounted. In that case useTransition will prefer to stay on the previous page for longer than to show it.

@gaearon thanks for the explanation and offer the mental model to think about it. Really appreciate it.

Why is useTransition not having any effect in this case despite having passed unstable_avoidThisFallback={true}?

You aren't running in Concurrent Mode.
Replace

ReactDOM.render(<App />, el)

with

ReactDOM.createRoot(el).render(<App />)

You aren't running in Concurrent Mode.
Replace

ReactDOM.render(<App />, el)

with

ReactDOM.createRoot(el).render(<App />)

That fixed the example, thank you very much Dan!

Was this page helpful?
0 / 5 - 0 ratings