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

Suppress hydration warnings when a preceding sibling suspends #24404

Merged
merged 14 commits into from Apr 20, 2022

Conversation

gnoff
Copy link
Collaborator

@gnoff gnoff commented Apr 19, 2022

Before this PR if there was a hydration error thrown in a ConcurrentMode tree the hydration procerss would enter a didSuspend state where further errors would be suppressed.

This did not previously happen however when a promise was thrown which would cause a Suspense boundary to suspend. Since it is typical for there to be hydration errors if a preceding sibling suspends because the server rendered content represented by that suspending component is not being claimed the didSuspend state is likely to follow quickly after (on the first non-promise thrown value)

This PR moves where the didSuspend state is activated to be for both Error and Promise thrown values.

closes #24384

@sizebot
Copy link

sizebot commented Apr 19, 2022

Comparing: 0dc4e66...be1cefa

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 = 131.52 kB 131.52 kB = 42.10 kB 42.10 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 136.79 kB 136.79 kB = 43.68 kB 43.67 kB
facebook-www/ReactDOM-prod.classic.js = 441.02 kB 441.02 kB = 80.44 kB 80.43 kB
facebook-www/ReactDOM-prod.modern.js = 426.28 kB 426.28 kB = 78.25 kB 78.26 kB
facebook-www/ReactDOMForked-prod.classic.js = 441.02 kB 441.02 kB = 80.44 kB 80.44 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against be1cefa

@gnoff gnoff force-pushed the hydration-warning-dev branch 2 times, most recently from ef55aca to cc0158c Compare April 20, 2022 00:22
Comment on lines +363 to +360
const secondToLastCall =
mockError.mock.calls[mockError.mock.calls.length - 2];
expect(secondToLastCall).toEqual([
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
'div',
'div',
'article',
'section',
'\n' +
' in div (at **)\n' +
' in article (at **)\n' +
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This warning used to be for trying to match the first Component div against the Text node. It was an expression of sorts of the issue brought up in #24384

This warning now is the intentional mismatch between article and div that the test introduces between client/server renders.

@@ -514,8 +518,6 @@ function throwException(
} else {
// This is a regular error, not a Suspense wakeable.
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
markDidSuspendWhileHydratingDEV();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this was just a mistake and it was meant to go into a Suspense-only path, but it went into an error-only path instead.

What happens if you move this to a Suspense-only path? Like line 455.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah i assumed it was intentionally opting into the warning suppression after the first hydration error by using suspend that way and didn't consider it was maybe just an oversight. seems like all tests pass.

I think this is sort of a noop in that by fake suspending on real error you opt out of future warns but the warns already opted out after the first one so in the end no observable difference

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it might be intentional because if an error is thrown, and nothing suspends, we still continue rendering siblings I believe. So those siblings would still be hydrating and causing more fake errors which would be confusing.

Do we have a test for that?

Really markDidSuspendWhileHydratingDEV is a misnomer because it's more like markDidThrowWhileHydratingDEV.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't recall writing a test for the error case but maybe. It does seem like it should be called in both cases, reason I asked whether it passes if you move it to be Suspense-only is I was curious if we already tested that

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

functionally they are equivelent (I checked in a test) but I agree the didThrow seems to map better onto what we expect to happen after a throw

If you error we would expect hydration to fail for later siblings since we likely didn't consume the current hydration step and will not advance.

If you Suspend we have the exact same logic

The reason this distinction is unobservable is the hydration warning will only ever log once and the errors are not suppressed so you end up with many thrown errors in either case and 1 logged error (didSuspend suppression or didAlreadyLog supression)

I'm in favor of reframing as didThrow and leaving it in both branches so I'll work on that unless someone wants to real me in

Copy link
Collaborator Author

@gnoff gnoff Apr 20, 2022

Choose a reason for hiding this comment

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

Well there is a test to suppress warnings after a hydration error

https://github.com/gnoff/react/blob/hydration-warning-dev/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js#L2993-L3005

Are you concerned about an error thrown from user code?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah I think we should have a test for a user error, too, since the fact that hydration mismatches cause an error internally is an implementation detail; it doesn't have to work that way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For example, the other way it could work is that we wait to walk the DOM tree until right before the commit phase. Then you'd be able to start rendering even before the HTML has arrived. In that case, we wouldn't know there was a mismatch until after the render phase had already completed. So instead of erroring, we'd throw out the work-in-progress and start again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah that makes sense. I’ll get another test in there soon

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Andrew is the new test covering what you hoped it would?

gnoff added 10 commits April 20, 2022 11:31
If a components suspends during hydration we expect there to be mismatches with server rendered HTML but we were not always supressing warning messages related to these expected mismatches
previously hydration would only be marked as supsending when a genuine error was thrown. This created an opportunity for a hydration mismatch that would warn after which later hydration mismatches would not lead to warnings. By moving the marker check earlier in the thrownException function we get the hydration context to enter the didSuspend state on both error and thrown promise cases which eliminates this gap.
This test was actually subject to the project identified in the issue fixed in this branch. After fixing the underlying issue the assertion logic needed to change to pick the right warning which now emits after hydration successfully completes on promise resolution. I changed the container type to 'section' to make the error message slightly easier to read/understand (for me)
For unknown reasons the didSuspend was being set only on the error path and nto the suspense path. The original change hoisted this to happen on both paths. This change moves the didSuspend call to the suspense path only. This appears to be a noop because if the error path marked didSuspend it would suppress later warnings but if it does not the warning functions themsevles do that suppression (after the first one which necessarily already happened)
the orignial behavior applied the hydration warning bailout to error paths only. originally I moved it to Suspense paths only but this commit restores it to both paths and renames the marker function as didThrow rather than didSuspend

The logic here is that for either case if we get a mismatch in hydration we want to warm up components but effectively consider the hydration for this boundary halted
@gaearon
Copy link
Collaborator

gaearon commented Apr 20, 2022

@gnoff Is this ready to merge or do you plan to add the test for the error case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants