Skip to content
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

Capture suspense boundaries with undefined fallbacks #21702

Merged
merged 1 commit into from
Jul 12, 2021

Conversation

rickhanlonii
Copy link
Member

Overview

This PR updates the behavior of the Suspense component so that it when the fallback is missing we still capture the boundary and renders null for the fallback.

Consider this code:

<Suspense fallback="outer"}>
  <Suspense>
    <AsyncText text="A"/>
  </Suspense>
</Suspense>,

Previously, since the inner Suspense boundary did not provide a fallback, we silently skipped it and kept going to the "outer" boundary. This is hard to debug and can be confusing.

Instead, we're going to capture the inner boundary and render an empty placeholder. This should make it more obvious to the user that they forgot to provide the fallback, and allow supporting use cases where that's what you intend to do.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Jun 18, 2021
@rickhanlonii rickhanlonii changed the title Capture suspense boundaries with undefined/null fallbacks Capture suspense boundaries with undefined fallbacks Jun 18, 2021
}
this.stack.push(frame);
return '';
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find a test that exercises this block, not could I figure out how to create one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make one. I had that on my TODO to test this parity.

@@ -1910,22 +1906,18 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
if (current === null) {
// Initial mount
// If we're currently hydrating, try to hydrate this boundary.
// But only if this has a fallback.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is reflected in the ReactDOMServerPartialHydration-test.internal.js test.

@@ -391,44 +391,10 @@ describe('ReactSuspense', () => {
expect(root).toMatchRenderedOutput('Hi');
});

it('only captures if `fallback` is defined', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to ReactSuspenseFallback-test.

@sizebot
Copy link

sizebot commented Jun 18, 2021

Comparing: 43f4cc1...614f1c6

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 127.34 kB 127.22 kB = 40.82 kB 40.78 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 130.16 kB 130.03 kB = 41.73 kB 41.73 kB
facebook-www/ReactDOM-prod.classic.js = 405.75 kB 405.48 kB = 75.02 kB 74.99 kB
facebook-www/ReactDOM-prod.modern.js = 394.18 kB 393.91 kB = 73.24 kB 73.20 kB
facebook-www/ReactDOMForked-prod.classic.js = 405.75 kB 405.48 kB = 75.03 kB 74.99 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactDOMServer-dev.classic.js = 153.44 kB 152.83 kB = 39.00 kB 38.91 kB
facebook-www/ReactDOMServer-prod.classic.js = 48.21 kB 47.87 kB = 11.24 kB 11.20 kB

Generated by 🚫 dangerJS against 614f1c6

React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
ReactCache = require('react-cache');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the helpers in ReactSuspenseWithNoopRenderer-test or ReactCache-test instead? Those are the closest to our preferred testing idioms. We're going to remove the react-cache package.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally just put them into those existing files. It tends to get annoying to update the helpers in different files that use the same patterns. For example we'll need to do a bunch of refactoring to change the avoidThisFallback API that needs to basically test all the same things again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put them in separate files because @acdlite mentioned that we should break out more tests into separate files for speed and organizational reasons.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, maybe the helpers should be broken out into a shared module too.

Organizationally I find that what organization we thought would make sense in terms of grouping features doesn't hold up for the next change.

The defacto pattern that has evolved that is pretty convenient is that files are grouped by the kind of test mechanism. So you can find a test with similar mechanism that you need and then copy it. It doesn't help speed so the way we solved that for the ServerIntegration tests is that we broke them out into a shared helper and then pulled that in everywhere.

It would also be nice if we didn't mix test mechanisms in the same file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main reason for suggesting more focused module was because some of them are getting really slow.

I've been resistant to the idea of having test helper modules because I don't want it to become a dumping ground for a bunch of idiosyncratic abstractions. I like that if there's a bad abstraction, it's confined to a single module.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really it's because I dislike when a test breaks and I have to open multiple files to figure out what the hell is going on, which is the case for some of our older server rendering tests

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheduler.yield things are a helper that is an example of doing it right so it can be done right.

I'm not a fan of the server rendering strategy per se but at least it's the same one every time. The ones that get really frustrating to upgrade from an older pattern are the ones using similar but slightly different patterns.

Who am I and what have I done to the good olde wet Seb?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately this is kind of a flaw of jest. We reset modules between every run anyway. There's no reason we couldn't run every individual test case in a separate process. That would also let us use proper ES modules since it would by default reset modules.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ones that get really frustrating to upgrade from an older pattern are the ones using similar but slightly different patterns.

I agree with that but I suppose I don't mind because in aggregate we spend more time debugging tests than we do making API changes.

In this particular case, though, I think the "async text" pattern has solidified enough that I'd be comfortable moving it to its own helper, a la yieldValue.

@rickhanlonii rickhanlonii force-pushed the rh-undefined-fallback branch 4 times, most recently from 37e6874 to b0c7eb3 Compare June 18, 2021 20:43
@rickhanlonii rickhanlonii force-pushed the rh-undefined-fallback branch from b0c7eb3 to 614f1c6 Compare June 18, 2021 21:00
@rickhanlonii rickhanlonii requested a review from acdlite June 18, 2021 21:33
@rickhanlonii rickhanlonii merged commit dc4a9b5 into facebook:master Jul 12, 2021
@rickhanlonii rickhanlonii deleted the rh-undefined-fallback branch July 12, 2021 18:26
@rickhanlonii rickhanlonii restored the rh-undefined-fallback branch July 12, 2021 18:27
@bvaughn
Copy link
Contributor

bvaughn commented Jul 13, 2021

Hey @rickhanlonii @acdlite Did you mean to merge this into master? I assume you meant main

sthagen added a commit to sthagen/facebook-react that referenced this pull request Jul 13, 2021
Capture suspense boundaries with undefined fallbacks (facebook#21702)
@zpao
Copy link
Member

zpao commented Jul 13, 2021

He fixed it with #21854. I am curious how master got re-involved here. The target ref should have been updated to main when the rename happened…

@rickhanlonii
Copy link
Member Author

@zpao there may have been a failure when doing the rename. I can't tell now because we deleted the branch, but you can see how in this old screenshot of react-native after the rename that there was a "rename failed" next to master:

image

@bvaughn
Copy link
Contributor

bvaughn commented Jul 26, 2021

Yeah, we later noticed that a lot of PRs failed to get renamed. I think the underlying cause has since been fixed (by Jon?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants