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

React 18: Non-recoverable hydration mismatch if mismatch occurs in the same boundary as main script #22833

Closed
eps1lon opened this issue Nov 25, 2021 · 12 comments
Labels
React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion

Comments

@eps1lon
Copy link
Collaborator

eps1lon commented Nov 25, 2021

React version: 18.0.0-rc.1-next-cb1e7b1c6-20220303

Steps To Reproduce

  1. Cause a hydration mismatch in the same boundary as <script /> for main entrypoint
  2. Attempt to hydrate

Link to code example: https://codesandbox.io/s/react-18-hydration-mismatch-in-document-6buos (based on https://codesandbox.io/s/kind-sammet-j56ro?file=/server/render.js:1054-1614 from reactwg/react-18#22)

{/* forced hydration mismatch */}
{typeof window === "undefined" ? <div>Server</div> : <span>Client</span>}
{/* main.js is the entrypoint for hydrateRoot */}
<script src={assets["main.js"]} />

The original issue was caused by a wrong usage of the Remix starter template:

<html lang="en">
	<head>
		<meta charSet="utf-8" />
		<meta name="viewport" content="width=device-width,initial-scale=1" />
		{title ? <title>{title}</title> : null}
		<Meta />
		<Links />
	</head>
	<body>
		{children}
		<Scripts />
		{process.env.NODE_ENV === "development" && <LiveReload />}
	</body>
</html>

The hydration mismatch happened due to a mismatch of process.env.NODE_ENV between client and server.

The current behavior

Whole UI is unmounted (i.e. no recovery via client-side only rendering) and the following errors are logged:

Errors when landing on the page
react-dom.development.js:14452 Uncaught Error: An error occurred during hydration. The server HTML was replaced with client content
    at throwOnHydrationMismatchIfConcurrentMode (react-dom.development.js:14452)
    at tryToClaimNextHydratableInstance (react-dom.development.js:14475)
    at updateHostComponent$1 (react-dom.development.js:20608)
    at beginWork (react-dom.development.js:22350)
    at HTMLUnknownElement.callCallback (react-dom.development.js:4128)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:4177)
    at invokeGuardedCallback (react-dom.development.js:4241)
    at beginWork$1 (react-dom.development.js:27125)
    at performUnitOfWork (react-dom.development.js:26290)
    at workLoopSync (react-dom.development.js:26200)
throwOnHydrationMismatchIfConcurrentMode @ react-dom.development.js:14452
tryToClaimNextHydratableInstance @ react-dom.development.js:14475
updateHostComponent$1 @ react-dom.development.js:20608
beginWork @ react-dom.development.js:22350
callCallback @ react-dom.development.js:4128
invokeGuardedCallbackDev @ react-dom.development.js:4177
invokeGuardedCallback @ react-dom.development.js:4241
beginWork$1 @ react-dom.development.js:27125
performUnitOfWork @ react-dom.development.js:26290
workLoopSync @ react-dom.development.js:26200
renderRootSync @ react-dom.development.js:26168
performConcurrentWorkOnRoot @ react-dom.development.js:25514
workLoop @ scheduler.development.js:265
flushWork @ scheduler.development.js:238
performWorkUntilDeadline @ scheduler.development.js:532
run @ setImmediate.js:40
runIfPresent @ setImmediate.js:69
onGlobalMessage @ setImmediate.js:109
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
client-hook-6.js:1 Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.
r.<computed> @ client-hook-6.js:1
printWarning @ react-dom.development.js:86
error @ react-dom.development.js:60
errorHydratingContainer @ react-dom.development.js:11234
recoverFromConcurrentError @ react-dom.development.js:25597
performConcurrentWorkOnRoot @ react-dom.development.js:25526
workLoop @ scheduler.development.js:265
flushWork @ scheduler.development.js:238
performWorkUntilDeadline @ scheduler.development.js:532
run @ setImmediate.js:40
runIfPresent @ setImmediate.js:69
onGlobalMessage @ setImmediate.js:109
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
react-dom.development.js:22649 Uncaught DOMException: Failed to execute 'appendChild' on 'Node': Only one element on document allowed.
    at appendChildToContainer (https://6buos.sse.codesandbox.io/main.js:11262:16)
    at insertOrAppendPlacementNodeIntoContainer (https://6buos.sse.codesandbox.io/main.js:24195:7)
    at insertOrAppendPlacementNodeIntoContainer (https://6buos.sse.codesandbox.io/main.js:24201:7)
    at commitPlacement (https://6buos.sse.codesandbox.io/main.js:24179:5)
    at commitMutationEffectsOnFiber (https://6buos.sse.codesandbox.io/main.js:24693:9)
    at commitMutationEffects_complete (https://6buos.sse.codesandbox.io/main.js:24586:7)
    at commitMutationEffects_begin (https://6buos.sse.codesandbox.io/main.js:24575:7)
    at commitMutationEffects (https://6buos.sse.codesandbox.io/main.js:24545:3)
    at commitRootImpl (https://6buos.sse.codesandbox.io/main.js:26919:5)
    at commitRoot (https://6buos.sse.codesandbox.io/main.js:26798:5)
(anonymous) @ react-dom.development.js:22649
callCallback @ react-dom.development.js:4128
invokeGuardedCallbackDev @ react-dom.development.js:4177
invokeGuardedCallback @ react-dom.development.js:4241
reportUncaughtErrorInDEV @ react-dom.development.js:22648
commitMutationEffects_complete @ react-dom.development.js:24200
commitMutationEffects_begin @ react-dom.development.js:24187
commitMutationEffects @ react-dom.development.js:24157
commitRootImpl @ react-dom.development.js:26531
commitRoot @ react-dom.development.js:26410
finishConcurrentRender @ react-dom.development.js:25728
performConcurrentWorkOnRoot @ react-dom.development.js:25574
workLoop @ scheduler.development.js:265
flushWork @ scheduler.development.js:238
performWorkUntilDeadline @ scheduler.development.js:532
run @ setImmediate.js:40
runIfPresent @ setImmediate.js:69
onGlobalMessage @ setImmediate.js:109
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
react-dom.development.js:22649 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
    at removeChildFromContainer (https://6buos.sse.codesandbox.io/main.js:11298:15)
    at unmountHostComponents (https://6buos.sse.codesandbox.io/main.js:24289:9)
    at commitDeletion (https://6buos.sse.codesandbox.io/main.js:24350:5)
    at commitMutationEffects_begin (https://6buos.sse.codesandbox.io/main.js:24561:11)
    at commitMutationEffects (https://6buos.sse.codesandbox.io/main.js:24545:3)
    at commitRootImpl (https://6buos.sse.codesandbox.io/main.js:26919:5)
    at commitRoot (https://6buos.sse.codesandbox.io/main.js:26798:5)
    at performSyncWorkOnRoot (https://6buos.sse.codesandbox.io/main.js:26248:3)
    at flushSyncCallbacks (https://6buos.sse.codesandbox.io/main.js:12196:22)
    at commitRootImpl (https://6buos.sse.codesandbox.io/main.js:27046:3)
(anonymous) @ react-dom.development.js:22649
callCallback @ react-dom.development.js:4128
invokeGuardedCallbackDev @ react-dom.development.js:4177
invokeGuardedCallback @ react-dom.development.js:4241
reportUncaughtErrorInDEV @ react-dom.development.js:22648
commitMutationEffects_begin @ react-dom.development.js:24175
commitMutationEffects @ react-dom.development.js:24157
commitRootImpl @ react-dom.development.js:26531
commitRoot @ react-dom.development.js:26410
performSyncWorkOnRoot @ react-dom.development.js:25860
flushSyncCallbacks @ react-dom.development.js:11803
commitRootImpl @ react-dom.development.js:26658
commitRoot @ react-dom.development.js:26410
finishConcurrentRender @ react-dom.development.js:25728
performConcurrentWorkOnRoot @ react-dom.development.js:25574
workLoop @ scheduler.development.js:265
flushWork @ scheduler.development.js:238
performWorkUntilDeadline @ scheduler.development.js:532
run @ setImmediate.js:40
runIfPresent @ setImmediate.js:69
onGlobalMessage @ setImmediate.js:109
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
client-hook-6.js:1 The above error occurred in the <App> component:

    at App (https://6buos.sse.codesandbox.io/main.js:34968:21)

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
r.<computed> @ client-hook-6.js:1
logCapturedError @ react-dom.development.js:18588
update.callback @ react-dom.development.js:18621
callCallback @ react-dom.development.js:13229
commitUpdateQueue @ react-dom.development.js:13250
commitLayoutEffectOnFiber @ react-dom.development.js:23177
commitLayoutMountEffects_complete @ react-dom.development.js:24431
commitLayoutEffects_begin @ react-dom.development.js:24417
commitLayoutEffects @ react-dom.development.js:24355
commitRootImpl @ react-dom.development.js:26544
commitRoot @ react-dom.development.js:26410
performSyncWorkOnRoot @ react-dom.development.js:25860
flushSyncCallbacks @ react-dom.development.js:11803
commitRootImpl @ react-dom.development.js:26658
commitRoot @ react-dom.development.js:26410
finishConcurrentRender @ react-dom.development.js:25728
performConcurrentWorkOnRoot @ react-dom.development.js:25574
workLoop @ scheduler.development.js:265
flushWork @ scheduler.development.js:238
performWorkUntilDeadline @ scheduler.development.js:532
run @ setImmediate.js:40
runIfPresent @ setImmediate.js:69
onGlobalMessage @ setImmediate.js:109
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
react-dom.development.js:11817 Uncaught DOMException: Failed to execute 'appendChild' on 'Node': Only one element on document allowed.
    at appendChildToContainer (https://6buos.sse.codesandbox.io/main.js:11262:16)
    at insertOrAppendPlacementNodeIntoContainer (https://6buos.sse.codesandbox.io/main.js:24195:7)
    at insertOrAppendPlacementNodeIntoContainer (https://6buos.sse.codesandbox.io/main.js:24201:7)
    at commitPlacement (https://6buos.sse.codesandbox.io/main.js:24179:5)
    at commitMutationEffectsOnFiber (https://6buos.sse.codesandbox.io/main.js:24693:9)
    at commitMutationEffects_complete (https://6buos.sse.codesandbox.io/main.js:24586:7)
    at commitMutationEffects_begin (https://6buos.sse.codesandbox.io/main.js:24575:7)
    at commitMutationEffects (https://6buos.sse.codesandbox.io/main.js:24545:3)
    at commitRootImpl (https://6buos.sse.codesandbox.io/main.js:26919:5)
    at commitRoot (https://6buos.sse.codesandbox.io/main.js:26798:5)
flushSyncCallbacks @ react-dom.development.js:11817
commitRootImpl @ react-dom.development.js:26658
commitRoot @ react-dom.development.js:26410
finishConcurrentRender @ react-dom.development.js:25728
performConcurrentWorkOnRoot @ react-dom.development.js:25574
workLoop @ scheduler.development.js:265
flushWork @ scheduler.development.js:238
performWorkUntilDeadline @ scheduler.development.js:532
run @ setImmediate.js:40
runIfPresent @ setImmediate.js:69
onGlobalMessage @ setImmediate.js:109
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
client-hook-6.js:1 The above error occurred in the <App> component:

    at App (https://6buos.sse.codesandbox.io/main.js:34968:21)

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
r.<computed> @ client-hook-6.js:1
logCapturedError @ react-dom.development.js:18588
update.callback @ react-dom.development.js:18621
callCallback @ react-dom.development.js:13229
commitUpdateQueue @ react-dom.development.js:13250
commitLayoutEffectOnFiber @ react-dom.development.js:23177
commitLayoutMountEffects_complete @ react-dom.development.js:24431
commitLayoutEffects_begin @ react-dom.development.js:24417
commitLayoutEffects @ react-dom.development.js:24355
commitRootImpl @ react-dom.development.js:26544
commitRoot @ react-dom.development.js:26410
performSyncWorkOnRoot @ react-dom.development.js:25860
flushSyncCallbacks @ react-dom.development.js:11803
postMessage (async)
registerImmediate @ setImmediate.js:120
setImmediate @ setImmediate.js:27
schedulePerformWorkUntilDeadline @ scheduler.development.js:563
requestHostCallback @ scheduler.development.js:587
unstable_scheduleCallback @ scheduler.development.js:440
scheduleCallback$1 @ react-dom.development.js:27211
ensureRootIsScheduled @ react-dom.development.js:25459
scheduleUpdateOnFiber @ react-dom.development.js:25278
updateContainer @ react-dom.development.js:28483
hydrateRoot @ react-dom.development.js:28991
./src/index.js @ index.js:12
__webpack_require__ @ bootstrap:19
0 @ index.js:12
__webpack_require__ @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ bootstrap:83
react-dom.development.js:11817 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
    at removeChildFromContainer (https://6buos.sse.codesandbox.io/main.js:11298:15)
    at unmountHostComponents (https://6buos.sse.codesandbox.io/main.js:24289:9)
    at commitDeletion (https://6buos.sse.codesandbox.io/main.js:24350:5)
    at commitMutationEffects_begin (https://6buos.sse.codesandbox.io/main.js:24561:11)
    at commitMutationEffects (https://6buos.sse.codesandbox.io/main.js:24545:3)
    at commitRootImpl (https://6buos.sse.codesandbox.io/main.js:26919:5)
    at commitRoot (https://6buos.sse.codesandbox.io/main.js:26798:5)
    at performSyncWorkOnRoot (https://6buos.sse.codesandbox.io/main.js:26248:3)
    at flushSyncCallbacks (https://6buos.sse.codesandbox.io/main.js:12196:22)
    at commitRootImpl (https://6buos.sse.codesandbox.io/main.js:27046:3)

The expected behavior

Not sure since I wouldn't be surprised if React couldn't do anything about it considering it probably unmounts its own script tag?

React 17 behavior: https://codesandbox.io/s/react-17-hydration-mismatch-in-document-m22vv

The good thing is that frameworks can guard against author error by wrapping user code in Suspense boundaries so that any hydration mismatch from user code does not result in throwing away the main script (or too much in general). But that might require a lot of otherwise unnecessary Suspense boundaries (is that a problem?)

@eps1lon eps1lon added Type: Discussion React 18 Bug reports, questions, and general feedback about React 18 labels Nov 25, 2021
@salazarm
Copy link
Contributor

salazarm commented Dec 2, 2021

The good thing is that frameworks can guard against author error by wrapping user code in Suspense boundaries so that any hydration mismatch from user code does not result in throwing away the main script (or too much in general).

Yeah thats why we think this is a reasonable fallback

But that might require a lot of otherwise unnecessary Suspense boundaries (is that a problem?)

It shouldn't be a problem, the overhead seems low? cc @sebmarkbage

@sebmarkbage
Copy link
Collaborator

I think this is probably a bug. At the root, we should probably client render or use the old strategy of patching up. The issue is that we don't know how many children belong to the root unless we insert comments.

The issue with Suspense boundaries is that they add to the semantics of the loading state so might see more than you should. We also have a concept of "backup boundaries" which are more suitable for this kind of thing. We haven't exposed them in the MVP though because we haven't finalized the API (avoidThisFallback). In general they're useful for enabling more fine grained partial hydration. The reason they necessary is because:

  • We don't know if we'll have all the code for that area so by ensuring that there is a fallback, we know what should happen in case the code doesn't exist. This is really an edge case though.

  • We could hydrate on a per component basis but then there's zero guarantee about cross-component relationships like if refs are resolved. By adding suspense boundaries you opt-in to refs being potentially null so it provides a valuable opt-in mechanism.

However, that just explains why we have backup boundaries for deeper in the tree.

They can also be used for any general purpose error handling on the server because they can render a temporary state while the client renders instead.

You probably shouldn't need that on the server though. It's probably better that you can have an error boundaries deal with it server side in that case. So that still leaves the case of client hydration mismatch.

@salazarm
Copy link
Contributor

salazarm commented Dec 6, 2021

The issue here is actually because we're hydrating into document. We end up hitting this error when trying to commit the client fallback:

Uncaught DOMException: Failed to execute 'appendChild' on 'Node': Only one element on document allowed.
    at appendChildToContainer (https://fl5fr.sse.codesandbox.io/main.js:11262:16)
    at insertOrAppendPlacementNodeIntoContainer (https://fl5fr.sse.codesandbox.io/main.js:24195:7)
    at insertOrAppendPlacementNodeIntoContainer (https://fl5fr.sse.codesandbox.io/main.js:24201:7)
    at commitPlacement (https://fl5fr.sse.codesandbox.io/main.js:24179:5)
    at commitMutationEffectsOnFiber (https://fl5fr.sse.codesandbox.io/main.js:24693:9)
    at commitMutationEffects_complete (https://fl5fr.sse.codesandbox.io/main.js:24586:7)
    at commitMutationEffects_begin (https://fl5fr.sse.codesandbox.io/main.js:24575:7)
    at commitMutationEffects (https://fl5fr.sse.codesandbox.io/main.js:24545:3)
    at commitRootImpl (https://fl5fr.sse.codesandbox.io/main.js:26919:5)
    at commitRoot (https://fl5fr.sse.codesandbox.io/main.js:26798:5)

Thinking through how to fix this

@jacob-ebey
Copy link

I believe this is related to the issues people are seeing around hydration when 3rd party browser extension modifies the DOM. Here is a reproduction case: https://github.com/jacob-ebey/react-18-hydration-bug

@gnoff
Copy link
Collaborator

gnoff commented May 10, 2022

@eps1lon can you confirm #24523 fixes this issue. I tested it here https://codesandbox.io/s/react-18-hydration-mismatch-in-document-fixed-24523-9n6zos?file=/package.json and believe it does.

@eps1lon
Copy link
Collaborator Author

eps1lon commented May 10, 2022

@eps1lon can you confirm #24523 fixes this issue. I tested it here codesandbox.io/s/react-18-hydration-mismatch-in-document-fixed-24523-9n6zos?file=/package.json and believe it does.

Sweet! Hydration errors are expected. But now we can at least recover.

@eps1lon eps1lon closed this as completed May 10, 2022
@gaearon
Copy link
Collaborator

gaearon commented Jun 14, 2022

This is out in 18.2.

@Ryongyon
Copy link

Half a year has passed, and the problem still exists

@Mihai-github
Copy link

+1

@raufabr
Copy link

raufabr commented Jul 19, 2023

Still having this issue in 18.2.

@jordanpowell88
Copy link

Hey all 👋 I work at Cypress and we have been trying to track down this on our end as well. Would be happy to assist on providing examples or work together on a fix for this.

@hanayashiki

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment