@samitouri / QOS-React-2 / commits / f7aa5e0aa3

Move Hydration Mismatch Errors to Throw or Log Once (Kind of) (#28502)

Stacked on #28476. We used to `console.error` for every mismatch we found, up until the error we threw for the hydration mismatch. This changes it so that we build up a set of diffs up until we either throw or complete hydrating the root/suspense boundary. If we throw, we append the diff to the error message which gets passed to onRecoverableError (which by default is also logged to console). If we complete, we append it to a `console.error`. Since we early abort when something throws, it effectively means that we can only collect multiple diffs if there were preceding non-throwing mismatches - i.e. only properties mismatched but tag name matched. There can still be multiple logs if multiple siblings Suspense boundaries all error hydrating but then they're separate errors entirely. We still log an extra line about something erroring but I think the goal should be that it leads to a single recoverable or console.error. This doesn't yet actually print the diff as part of this message. That's in a follow up PR.

Sebastian Markbåge committed Mar 26, 2024 at 17:01 UTC f7aa5e0aa3e2aa51279af4b6cb5413912cacd7f5
16 files changed +1207 -1271
packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
+8 -2
@@ -182,7 +182,8 @@ describe('ReactDOMFizzForm', () => {
182 ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
183 });
184 }).toErrorDev(
185 - 'Prop `action` did not match. Server: "function" Client: "action"',
185 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
186 + {withoutStack: true},
187 );
188 });
189
@@ -344,7 +345,12 @@ describe('ReactDOMFizzForm', () => {
345 await act(async () => {
346 root = ReactDOMClient.hydrateRoot(container, <App />);
347 });
347 - }).toErrorDev(['Prop `formTarget` did not match.']);
348 + }).toErrorDev(
349 + [
350 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
351 + ],
352 + {withoutStack: true},
353 + );
354 await act(async () => {
355 root.render(<App isUpdate={true} />);
356 });
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+66 -74
@@ -47,6 +47,15 @@ let waitForPaint;
47 let clientAct;
48 let streamingContainer;
49
50 +function normalizeError(msg) {
51 + // Take the first sentence to make it easier to assert on.
52 + const idx = msg.indexOf('.');
53 + if (idx > -1) {
54 + return msg.slice(0, idx + 1);
55 + }
56 + return msg;
57 +}
58 +
59 describe('ReactDOMFizzServer', () => {
60 beforeEach(() => {
61 jest.resetModules();
@@ -2391,7 +2400,9 @@ describe('ReactDOMFizzServer', () => {
2400
2401 ReactDOMClient.hydrateRoot(container, <App />, {
2402 onRecoverableError(error) {
2394 - Scheduler.log('Log recoverable error: ' + error.message);
2403 + Scheduler.log(
2404 + 'Log recoverable error: ' + normalizeError(error.message),
2405 + );
2406 },
2407 });
2408
@@ -2399,18 +2410,12 @@ describe('ReactDOMFizzServer', () => {
2410 // The first paint switches to client rendering due to mismatch
2411 await waitForPaint([
2412 'client',
2402 - 'Log recoverable error: Hydration failed because the initial ' +
2403 - 'UI does not match what was rendered on the server.',
2404 - 'Log recoverable error: There was an error while hydrating. ' +
2405 - 'Because the error happened outside of a Suspense boundary, the ' +
2406 - 'entire root will switch to client rendering.',
2413 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
2414 + 'Log recoverable error: There was an error while hydrating.',
2415 ]);
2416 }).toErrorDev(
2417 [
2418 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
2411 - 'Warning: Expected server HTML to contain a matching <div> in the root.\n' +
2412 - ' in div (at **)\n' +
2413 - ' in App (at **)',
2419 ],
2420 {withoutStack: 1},
2421 );
@@ -2474,7 +2479,9 @@ describe('ReactDOMFizzServer', () => {
2479
2480 ReactDOMClient.hydrateRoot(container, <App />, {
2481 onRecoverableError(error) {
2477 - Scheduler.log('Log recoverable error: ' + error.message);
2482 + Scheduler.log(
2483 + 'Log recoverable error: ' + normalizeError(error.message),
2484 + );
2485 },
2486 });
2487
@@ -2483,18 +2490,12 @@ describe('ReactDOMFizzServer', () => {
2490 // The first paint switches to client rendering due to mismatch
2491 await waitForPaint([
2492 'client',
2486 - 'Log recoverable error: Hydration failed because the initial ' +
2487 - 'UI does not match what was rendered on the server.',
2488 - 'Log recoverable error: There was an error while hydrating. ' +
2489 - 'Because the error happened outside of a Suspense boundary, the ' +
2490 - 'entire root will switch to client rendering.',
2493 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
2494 + 'Log recoverable error: There was an error while hydrating.',
2495 ]);
2496 }).toErrorDev(
2497 [
2498 'Warning: An error occurred during hydration. The server HTML was replaced with client content',
2495 - 'Warning: Expected server HTML to contain a matching <div> in the root.\n' +
2496 - ' in div (at **)\n' +
2497 - ' in App (at **)',
2499 ],
2500 {withoutStack: 1},
2501 );
@@ -2557,7 +2558,7 @@ describe('ReactDOMFizzServer', () => {
2558 isClient = true;
2559 ReactDOMClient.hydrateRoot(container, <App />, {
2560 onRecoverableError(error) {
2560 - Scheduler.log(error.message);
2561 + Scheduler.log(normalizeError(error.message));
2562 },
2563 });
2564
@@ -2567,9 +2568,7 @@ describe('ReactDOMFizzServer', () => {
2568 await waitForAll([
2569 'Yay!',
2570 'Hydration error',
2570 - 'There was an error while hydrating. Because the error happened ' +
2571 - 'outside of a Suspense boundary, the entire root will switch ' +
2572 - 'to client rendering.',
2571 + 'There was an error while hydrating.',
2572 ]);
2573 }).toErrorDev(
2574 'An error occurred during hydration. The server HTML was replaced',
@@ -2739,7 +2738,7 @@ describe('ReactDOMFizzServer', () => {
2738 isClient = true;
2739 ReactDOMClient.hydrateRoot(container, <App />, {
2740 onRecoverableError(error) {
2742 - Scheduler.log(error.message);
2741 + Scheduler.log(normalizeError(error.message));
2742 },
2743 });
2744
@@ -2748,7 +2747,7 @@ describe('ReactDOMFizzServer', () => {
2747 await waitForAll([
2748 'Yay!',
2749 'Hydration error',
2751 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
2750 + 'There was an error while hydrating this Suspense boundary.',
2751 ]);
2752 expect(getVisibleChildren(container)).toEqual(
2753 <div>
@@ -3194,7 +3193,7 @@ describe('ReactDOMFizzServer', () => {
3193 isClient = true;
3194 ReactDOMClient.hydrateRoot(container, <App />, {
3195 onRecoverableError(error) {
3197 - Scheduler.log(error.message);
3196 + Scheduler.log(normalizeError(error.message));
3197 },
3198 });
3199
@@ -3202,8 +3201,7 @@ describe('ReactDOMFizzServer', () => {
3201 // to client rendering.
3202 await waitForAll([
3203 'Hydration error',
3205 - 'There was an error while hydrating this Suspense boundary. Switched ' +
3206 - 'to client rendering.',
3204 + 'There was an error while hydrating this Suspense boundary.',
3205 ]);
3206 expect(getVisibleChildren(container)).toEqual(
3207 <div>
@@ -3263,7 +3261,9 @@ describe('ReactDOMFizzServer', () => {
3261
3262 const root = ReactDOMClient.createRoot(container, {
3263 onRecoverableError(error) {
3266 - Scheduler.log('Logged a recoverable error: ' + error.message);
3264 + Scheduler.log(
3265 + 'Logged a recoverable error: ' + normalizeError(error.message),
3266 + );
3267 },
3268 });
3269 React.startTransition(() => {
@@ -3339,7 +3339,9 @@ describe('ReactDOMFizzServer', () => {
3339 isClient = true;
3340 ReactDOMClient.hydrateRoot(container, <App />, {
3341 onRecoverableError(error) {
3342 - Scheduler.log('Logged recoverable error: ' + error.message);
3342 + Scheduler.log(
3343 + 'Logged recoverable error: ' + normalizeError(error.message),
3344 + );
3345 },
3346 });
3347
@@ -3349,11 +3351,11 @@ describe('ReactDOMFizzServer', () => {
3351
3352 'Logged recoverable error: Hydration error',
3353 'Logged recoverable error: There was an error while hydrating this ' +
3352 - 'Suspense boundary. Switched to client rendering.',
3354 + 'Suspense boundary.',
3355
3356 'Logged recoverable error: Hydration error',
3357 'Logged recoverable error: There was an error while hydrating this ' +
3356 - 'Suspense boundary. Switched to client rendering.',
3358 + 'Suspense boundary.',
3359 ]);
3360 });
3361
@@ -4395,7 +4397,9 @@ describe('ReactDOMFizzServer', () => {
4397 const [ClientApp, clientResolve] = makeApp();
4398 ReactDOMClient.hydrateRoot(container, <ClientApp />, {
4399 onRecoverableError(error) {
4398 - Scheduler.log('Logged recoverable error: ' + error.message);
4400 + Scheduler.log(
4401 + 'Logged recoverable error: ' + normalizeError(error.message),
4402 + );
4403 },
4404 });
4405 await waitForAll([]);
@@ -4471,7 +4475,9 @@ describe('ReactDOMFizzServer', () => {
4475 const [ClientApp, clientResolve] = makeApp();
4476 ReactDOMClient.hydrateRoot(container, <ClientApp text="replaced" />, {
4477 onRecoverableError(error) {
4474 - Scheduler.log('Logged recoverable error: ' + error.message);
4478 + Scheduler.log(
4479 + 'Logged recoverable error: ' + normalizeError(error.message),
4480 + );
4481 },
4482 });
4483 await waitForAll([]);
@@ -4486,14 +4492,10 @@ describe('ReactDOMFizzServer', () => {
4492 // Now that the boundary resolves to it's children the hydration completes and discovers that there is a mismatch requiring
4493 // client-side rendering.
4494 await clientResolve();
4489 - await expect(async () => {
4490 - await waitForAll([
4491 - 'Logged recoverable error: Text content does not match server-rendered HTML.',
4492 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4493 - ]);
4494 - }).toErrorDev(
4495 - 'Warning: Text content did not match. Server: "initial" Client: "replaced',
4496 - );
4495 + await waitForAll([
4496 + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
4497 + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4498 + ]);
4499 expect(getVisibleChildren(container)).toEqual(
4500 <div>
4501 <p>A</p>
@@ -4539,12 +4541,14 @@ describe('ReactDOMFizzServer', () => {
4541
4542 ReactDOMClient.hydrateRoot(container, <App text="replaced" />, {
4543 onRecoverableError(error) {
4542 - Scheduler.log('Logged recoverable error: ' + error.message);
4544 + Scheduler.log(
4545 + 'Logged recoverable error: ' + normalizeError(error.message),
4546 + );
4547 },
4548 });
4549 await waitForAll([
4546 - 'Logged recoverable error: Text content does not match server-rendered HTML.',
4547 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4550 + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
4551 + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4552 ]);
4553
4554 expect(getVisibleChildren(container)).toEqual(
@@ -4556,21 +4560,7 @@ describe('ReactDOMFizzServer', () => {
4560 );
4561
4562 await waitForAll([]);
4559 - if (__DEV__) {
4560 - expect(mockError.mock.calls.length).toBe(1);
4561 - expect(mockError.mock.calls[0]).toEqual([
4562 - 'Warning: Text content did not match. Server: "%s" Client: "%s"%s',
4563 - 'initial',
4564 - 'replaced',
4565 - '\n' +
4566 - ' in h2 (at **)\n' +
4567 - ' in Suspense (at **)\n' +
4568 - ' in div (at **)\n' +
4569 - ' in App (at **)',
4570 - ]);
4571 - } else {
4572 - expect(mockError.mock.calls.length).toBe(0);
4573 - }
4563 + expect(mockError.mock.calls.length).toBe(0);
4564 } finally {
4565 console.error = originalConsoleError;
4566 }
@@ -4626,12 +4616,14 @@ describe('ReactDOMFizzServer', () => {
4616
4617 ReactDOMClient.hydrateRoot(container, <App />, {
4618 onRecoverableError(error) {
4629 - Scheduler.log('Logged recoverable error: ' + error.message);
4619 + Scheduler.log(
4620 + 'Logged recoverable error: ' + normalizeError(error.message),
4621 + );
4622 },
4623 });
4624 await waitForAll([
4625 'Logged recoverable error: uh oh',
4634 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4626 + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4627 ]);
4628
4629 expect(getVisibleChildren(container)).toEqual(
@@ -4713,7 +4705,9 @@ describe('ReactDOMFizzServer', () => {
4705
4706 ReactDOMClient.hydrateRoot(container, <App />, {
4707 onRecoverableError(error) {
4716 - Scheduler.log('Logged recoverable error: ' + error.message);
4708 + Scheduler.log(
4709 + 'Logged recoverable error: ' + normalizeError(error.message),
4710 + );
4711 },
4712 });
4713 await waitForAll([
@@ -4722,7 +4716,7 @@ describe('ReactDOMFizzServer', () => {
4716 // onRecoverableError because the UI recovered without surfacing the
4717 // error to the user.
4718 'Logged recoverable error: first error',
4725 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4719 + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4720 ]);
4721 expect(mockError.mock.calls).toEqual([]);
4722 mockError.mockClear();
@@ -4830,7 +4824,9 @@ describe('ReactDOMFizzServer', () => {
4824
4825 ReactDOMClient.hydrateRoot(container, <App />, {
4826 onRecoverableError(error) {
4833 - Scheduler.log('Logged recoverable error: ' + error.message);
4827 + Scheduler.log(
4828 + 'Logged recoverable error: ' + normalizeError(error.message),
4829 + );
4830 },
4831 });
4832 await waitForAll(['suspending']);
@@ -4847,7 +4843,7 @@ describe('ReactDOMFizzServer', () => {
4843 await waitForAll([
4844 'throwing: first error',
4845 'Logged recoverable error: first error',
4850 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4846 + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4847 ]);
4848 expect(getVisibleChildren(container)).toEqual(
4849 <div>
@@ -4954,14 +4950,16 @@ describe('ReactDOMFizzServer', () => {
4950
4951 ReactDOMClient.hydrateRoot(container, <App />, {
4952 onRecoverableError(error) {
4957 - Scheduler.log('Logged recoverable error: ' + error.message);
4953 + Scheduler.log(
4954 + 'Logged recoverable error: ' + normalizeError(error.message),
4955 + );
4956 },
4957 });
4958 await waitForAll([
4959 'throwing: first error',
4960 'suspending',
4961 'Logged recoverable error: first error',
4964 - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
4962 + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.',
4963 ]);
4964 expect(mockError.mock.calls).toEqual([]);
4965 mockError.mockClear();
@@ -6341,13 +6339,7 @@ describe('ReactDOMFizzServer', () => {
6339 });
6340 await expect(async () => {
6341 await waitForAll([]);
6344 - }).toErrorDev(
6345 - [
6346 - 'Expected server HTML to contain a matching <span> in the root',
6347 - 'An error occurred during hydration',
6348 - ],
6349 - {withoutStack: 1},
6350 - );
6342 + }).toErrorDev(['An error occurred during hydration'], {withoutStack: 1});
6343 expect(errors.length).toEqual(2);
6344 expect(getVisibleChildren(container)).toEqual(<span />);
6345 });
packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js
+35 -33
@@ -24,6 +24,15 @@ let hasErrored = false;
24 let fatalError = undefined;
25 let waitForAll;
26
27 +function normalizeError(msg) {
28 + // Take the first sentence to make it easier to assert on.
29 + const idx = msg.indexOf('.');
30 + if (idx > -1) {
31 + return msg.slice(0, idx + 1);
32 + }
33 + return msg;
34 +}
35 +
36 describe('ReactDOMFizzServerHydrationWarning', () => {
37 beforeEach(() => {
38 jest.resetModules();
@@ -156,7 +165,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
165 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
166 onRecoverableError(error) {
167 // Don't miss a hydration error. There should be none.
159 - Scheduler.log(error.message);
168 + Scheduler.log(normalizeError(error.message));
169 },
170 });
171 await waitForAll([]);
@@ -196,7 +205,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
205 );
206 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
207 onRecoverableError(error) {
199 - Scheduler.log(error.message);
208 + Scheduler.log(normalizeError(error.message));
209 },
210 });
211 await waitForAll([]);
@@ -237,17 +246,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
246 );
247 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
248 onRecoverableError(error) {
240 - Scheduler.log(error.message);
249 + Scheduler.log(normalizeError(error.message));
250 },
251 });
252 await expect(async () => {
253 await waitForAll([
245 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
246 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
254 + "Hydration failed because the server rendered HTML didn't match the client.",
255 + 'There was an error while hydrating.',
256 ]);
257 }).toErrorDev(
258 [
250 - 'Expected server HTML to contain a matching <span> in <span>',
259 'An error occurred during hydration. The server HTML was replaced with client content.',
260 ],
261 {withoutStack: 1},
@@ -282,7 +290,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
290 );
291 const root = ReactDOMClient.hydrateRoot(container, <App text="Client" />, {
292 onRecoverableError(error) {
285 - Scheduler.log(error.message);
293 + Scheduler.log(normalizeError(error.message));
294 },
295 });
296 await waitForAll([]);
@@ -326,17 +334,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
334 );
335 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
336 onRecoverableError(error) {
329 - Scheduler.log(error.message);
337 + Scheduler.log(normalizeError(error.message));
338 },
339 });
340 await expect(async () => {
341 await waitForAll([
334 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
335 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
342 + "Hydration failed because the server rendered HTML didn't match the client.",
343 + 'There was an error while hydrating.',
344 ]);
345 }).toErrorDev(
346 [
339 - 'Did not expect server HTML to contain the text node "Server" in <span>',
347 'An error occurred during hydration. The server HTML was replaced with client content.',
348 ],
349 {withoutStack: 1},
@@ -374,17 +381,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
381 );
382 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
383 onRecoverableError(error) {
377 - Scheduler.log(error.message);
384 + Scheduler.log(normalizeError(error.message));
385 },
386 });
387 await expect(async () => {
388 await waitForAll([
382 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
383 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
389 + "Hydration failed because the server rendered HTML didn't match the client.",
390 + 'There was an error while hydrating.',
391 ]);
392 }).toErrorDev(
393 [
387 - 'Expected server HTML to contain a matching text node for "Client" in <span>.',
394 'An error occurred during hydration. The server HTML was replaced with client content.',
395 ],
396 {withoutStack: 1},
@@ -425,17 +431,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
431 );
432 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
433 onRecoverableError(error) {
428 - Scheduler.log(error.message);
434 + Scheduler.log(normalizeError(error.message));
435 },
436 });
437 await expect(async () => {
438 await waitForAll([
433 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
434 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
439 + "Hydration failed because the server rendered HTML didn't match the client.",
440 + 'There was an error while hydrating.',
441 ]);
442 }).toErrorDev(
443 [
438 - 'Did not expect server HTML to contain the text node "Server" in <span>.',
444 'An error occurred during hydration. The server HTML was replaced with client content.',
445 ],
446 {withoutStack: 1},
@@ -474,17 +479,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
479 );
480 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
481 onRecoverableError(error) {
477 - Scheduler.log(error.message);
482 + Scheduler.log(normalizeError(error.message));
483 },
484 });
485 await expect(async () => {
486 await waitForAll([
482 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
483 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
487 + "Hydration failed because the server rendered HTML didn't match the client.",
488 + 'There was an error while hydrating.',
489 ]);
490 }).toErrorDev(
491 [
487 - 'Expected server HTML to contain a matching text node for "Client" in <span>.',
492 'An error occurred during hydration. The server HTML was replaced with client content.',
493 ],
494 {withoutStack: 1},
@@ -527,7 +531,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
531 );
532 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
533 onRecoverableError(error) {
530 - Scheduler.log(error.message);
534 + Scheduler.log(normalizeError(error.message));
535 },
536 });
537 await waitForAll([]);
@@ -564,7 +568,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
568 );
569 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
570 onRecoverableError(error) {
567 - Scheduler.log(error.message);
571 + Scheduler.log(normalizeError(error.message));
572 },
573 });
574 await waitForAll([]);
@@ -597,17 +601,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
601 );
602 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
603 onRecoverableError(error) {
600 - Scheduler.log(error.message);
604 + Scheduler.log(normalizeError(error.message));
605 },
606 });
607 await expect(async () => {
608 await waitForAll([
605 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
606 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
609 + "Hydration failed because the server rendered HTML didn't match the client.",
610 + 'There was an error while hydrating.',
611 ]);
612 }).toErrorDev(
613 [
610 - 'Expected server HTML to contain a matching <p> in <div>.',
614 'An error occurred during hydration. The server HTML was replaced with client content.',
615 ],
616 {withoutStack: 1},
@@ -643,17 +646,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
646 );
647 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
648 onRecoverableError(error) {
646 - Scheduler.log(error.message);
649 + Scheduler.log(normalizeError(error.message));
650 },
651 });
652 await expect(async () => {
653 await waitForAll([
651 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
652 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
654 + "Hydration failed because the server rendered HTML didn't match the client.",
655 + 'There was an error while hydrating.',
656 ]);
657 }).toErrorDev(
658 [
656 - 'Did not expect server HTML to contain a <p> in <div>.',
659 'An error occurred during hydration. The server HTML was replaced with client content.',
660 ],
661 {withoutStack: 1},
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
-2
@@ -6480,7 +6480,6 @@ body {
6480 await waitForAll([]);
6481 }).toErrorDev(
6482 [
6483 - 'Warning: Text content did not match. Server: "server" Client: "client"',
6483 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
6484 ],
6485 {withoutStack: 1},
@@ -8270,7 +8269,6 @@ background-color: green;
8269 await waitForAll([]);
8270 }).toErrorDev(
8271 [
8273 - 'Warning: Text content did not match. Server: "server" Client: "client"',
8272 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
8273 ],
8274 {withoutStack: 1},
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+516 -264
@@ -82,12 +82,19 @@ describe('ReactDOMServerHydration', () => {
82 }
83 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
84 [
85 - "Warning: Text content did not match. Server: "server" Client: "client"
86 - in main (at **)
87 - in div (at **)
88 - in Mismatch (at **)",
85 "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
90 - "Caught [Text content does not match server-rendered HTML.]",
86 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
87 +
88 + - A server/client branch \`if (typeof window !== 'undefined')\`.
89 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
90 + - Date formatting in a user's locale which doesn't match the server.
91 + - External changing data without sending a snapshot of it along with the HTML.
92 + - Invalid HTML tag nesting.
93 +
94 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
95 +
96 + https://react.dev/link/hydration-mismatch
97 + ]",
98 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
99 ]
100 `);
@@ -109,11 +116,19 @@ describe('ReactDOMServerHydration', () => {
116 /* eslint-disable no-irregular-whitespace */
117 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
118 [
112 - "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text"
113 - in div (at **)
114 - in Mismatch (at **)",
119 "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
116 - "Caught [Text content does not match server-rendered HTML.]",
120 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
121 +
122 + - A server/client branch \`if (typeof window !== 'undefined')\`.
123 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
124 + - Date formatting in a user's locale which doesn't match the server.
125 + - External changing data without sending a snapshot of it along with the HTML.
126 + - Invalid HTML tag nesting.
127 +
128 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
129 +
130 + https://react.dev/link/hydration-mismatch
131 + ]",
132 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
133 ]
134 `);
@@ -138,10 +153,18 @@ describe('ReactDOMServerHydration', () => {
153 }
154 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
155 [
141 - "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: {"__html":"<span>server</span>"} Client: {"__html":"<span>client</span>"}
142 - in main (at **)
143 - in div (at **)
144 - in Mismatch (at **)",
156 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
157 +
158 + - A server/client branch \`if (typeof window !== 'undefined')\`.
159 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
160 + - Date formatting in a user's locale which doesn't match the server.
161 + - External changing data without sending a snapshot of it along with the HTML.
162 + - Invalid HTML tag nesting.
163 +
164 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
165 +
166 + https://react.dev/link/hydration-mismatch
167 + ",
168 ]
169 `);
170 });
@@ -162,10 +185,18 @@ describe('ReactDOMServerHydration', () => {
185 }
186 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
187 [
165 - "Warning: Prop \`className\` did not match. Server: "child server" Client: "child client"
166 - in main (at **)
167 - in div (at **)
168 - in Mismatch (at **)",
188 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
189 +
190 + - A server/client branch \`if (typeof window !== 'undefined')\`.
191 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
192 + - Date formatting in a user's locale which doesn't match the server.
193 + - External changing data without sending a snapshot of it along with the HTML.
194 + - Invalid HTML tag nesting.
195 +
196 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
197 +
198 + https://react.dev/link/hydration-mismatch
199 + ",
200 ]
201 `);
202 });
@@ -185,10 +216,18 @@ describe('ReactDOMServerHydration', () => {
216 }
217 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
218 [
188 - "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1
189 - in main (at **)
190 - in div (at **)
191 - in Mismatch (at **)",
219 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
220 +
221 + - A server/client branch \`if (typeof window !== 'undefined')\`.
222 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
223 + - Date formatting in a user's locale which doesn't match the server.
224 + - External changing data without sending a snapshot of it along with the HTML.
225 + - Invalid HTML tag nesting.
226 +
227 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
228 +
229 + https://react.dev/link/hydration-mismatch
230 + ",
231 ]
232 `);
233 });
@@ -208,10 +247,18 @@ describe('ReactDOMServerHydration', () => {
247 }
248 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
249 [
211 - "Warning: Extra attribute from the server: tabindex
212 - in main (at **)
213 - in div (at **)
214 - in Mismatch (at **)",
250 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
251 +
252 + - A server/client branch \`if (typeof window !== 'undefined')\`.
253 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
254 + - Date formatting in a user's locale which doesn't match the server.
255 + - External changing data without sending a snapshot of it along with the HTML.
256 + - Invalid HTML tag nesting.
257 +
258 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
259 +
260 + https://react.dev/link/hydration-mismatch
261 + ",
262 ]
263 `);
264 });
@@ -231,10 +278,18 @@ describe('ReactDOMServerHydration', () => {
278 }
279 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
280 [
234 - "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1
235 - in main (at **)
236 - in div (at **)
237 - in Mismatch (at **)",
281 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
282 +
283 + - A server/client branch \`if (typeof window !== 'undefined')\`.
284 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
285 + - Date formatting in a user's locale which doesn't match the server.
286 + - External changing data without sending a snapshot of it along with the HTML.
287 + - Invalid HTML tag nesting.
288 +
289 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
290 +
291 + https://react.dev/link/hydration-mismatch
292 + ",
293 ]
294 `);
295 });
@@ -255,10 +310,18 @@ describe('ReactDOMServerHydration', () => {
310 }
311 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
312 [
258 - "Warning: Prop \`style\` did not match. Server: {"opacity":"0"} Client: {"opacity":1}
259 - in main (at **)
260 - in div (at **)
261 - in Mismatch (at **)",
313 + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
314 +
315 + - A server/client branch \`if (typeof window !== 'undefined')\`.
316 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
317 + - Date formatting in a user's locale which doesn't match the server.
318 + - External changing data without sending a snapshot of it along with the HTML.
319 + - Invalid HTML tag nesting.
320 +
321 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
322 +
323 + https://react.dev/link/hydration-mismatch
324 + ",
325 ]
326 `);
327 });
@@ -276,16 +339,23 @@ describe('ReactDOMServerHydration', () => {
339 );
340 }
341 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
279 - [
280 - "Warning: Expected server HTML to contain a matching <main> in <div>.
281 - in main (at **)
282 - in div (at **)
283 - in Mismatch (at **)",
284 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
285 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
286 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
287 - ]
288 - `);
342 + [
343 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
344 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
345 +
346 + - A server/client branch \`if (typeof window !== 'undefined')\`.
347 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
348 + - Date formatting in a user's locale which doesn't match the server.
349 + - External changing data without sending a snapshot of it along with the HTML.
350 + - Invalid HTML tag nesting.
351 +
352 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
353 +
354 + https://react.dev/link/hydration-mismatch
355 + ]",
356 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
357 + ]
358 + `);
359 });
360
361 // @gate __DEV__
@@ -300,16 +370,23 @@ describe('ReactDOMServerHydration', () => {
370 );
371 }
372 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
303 - [
304 - "Warning: Expected server HTML to contain a matching <header> in <div>.
305 - in header (at **)
306 - in div (at **)
307 - in Mismatch (at **)",
308 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
309 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
310 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
311 - ]
312 - `);
373 + [
374 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
375 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
376 +
377 + - A server/client branch \`if (typeof window !== 'undefined')\`.
378 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
379 + - Date formatting in a user's locale which doesn't match the server.
380 + - External changing data without sending a snapshot of it along with the HTML.
381 + - Invalid HTML tag nesting.
382 +
383 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
384 +
385 + https://react.dev/link/hydration-mismatch
386 + ]",
387 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
388 + ]
389 + `);
390 });
391
392 // @gate __DEV__
@@ -324,16 +401,23 @@ describe('ReactDOMServerHydration', () => {
401 );
402 }
403 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
327 - [
328 - "Warning: Expected server HTML to contain a matching <main> in <div>.
329 - in main (at **)
330 - in div (at **)
331 - in Mismatch (at **)",
332 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
333 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
334 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
335 - ]
336 - `);
404 + [
405 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
406 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
407 +
408 + - A server/client branch \`if (typeof window !== 'undefined')\`.
409 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
410 + - Date formatting in a user's locale which doesn't match the server.
411 + - External changing data without sending a snapshot of it along with the HTML.
412 + - Invalid HTML tag nesting.
413 +
414 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
415 +
416 + https://react.dev/link/hydration-mismatch
417 + ]",
418 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
419 + ]
420 + `);
421 });
422
423 // @gate __DEV__
@@ -348,16 +432,23 @@ describe('ReactDOMServerHydration', () => {
432 );
433 }
434 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
351 - [
352 - "Warning: Expected server HTML to contain a matching <footer> in <div>.
353 - in footer (at **)
354 - in div (at **)
355 - in Mismatch (at **)",
356 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
357 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
358 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
359 - ]
360 - `);
435 + [
436 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
437 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
438 +
439 + - A server/client branch \`if (typeof window !== 'undefined')\`.
440 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
441 + - Date formatting in a user's locale which doesn't match the server.
442 + - External changing data without sending a snapshot of it along with the HTML.
443 + - Invalid HTML tag nesting.
444 +
445 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
446 +
447 + https://react.dev/link/hydration-mismatch
448 + ]",
449 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
450 + ]
451 + `);
452 });
453 });
454
@@ -369,11 +460,19 @@ describe('ReactDOMServerHydration', () => {
460 }
461 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
462 [
372 - "Warning: Text content did not match. Server: "" Client: "only"
373 - in div (at **)
374 - in Mismatch (at **)",
463 "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
376 - "Caught [Text content does not match server-rendered HTML.]",
464 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
465 +
466 + - A server/client branch \`if (typeof window !== 'undefined')\`.
467 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
468 + - Date formatting in a user's locale which doesn't match the server.
469 + - External changing data without sending a snapshot of it along with the HTML.
470 + - Invalid HTML tag nesting.
471 +
472 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
473 +
474 + https://react.dev/link/hydration-mismatch
475 + ]",
476 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
477 ]
478 `);
@@ -391,15 +490,23 @@ describe('ReactDOMServerHydration', () => {
490 );
491 }
492 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
394 - [
395 - "Warning: Expected server HTML to contain a matching text node for "second" in <div>.
396 - in div (at **)
397 - in Mismatch (at **)",
398 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
399 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
400 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
401 - ]
402 - `);
493 + [
494 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
495 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
496 +
497 + - A server/client branch \`if (typeof window !== 'undefined')\`.
498 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
499 + - Date formatting in a user's locale which doesn't match the server.
500 + - External changing data without sending a snapshot of it along with the HTML.
501 + - Invalid HTML tag nesting.
502 +
503 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
504 +
505 + https://react.dev/link/hydration-mismatch
506 + ]",
507 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
508 + ]
509 + `);
510 });
511
512 // @gate __DEV__
@@ -414,15 +521,23 @@ describe('ReactDOMServerHydration', () => {
521 );
522 }
523 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
417 - [
418 - "Warning: Expected server HTML to contain a matching text node for "first" in <div>.
419 - in div (at **)
420 - in Mismatch (at **)",
421 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
422 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
423 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
424 - ]
425 - `);
524 + [
525 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
526 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
527 +
528 + - A server/client branch \`if (typeof window !== 'undefined')\`.
529 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
530 + - Date formatting in a user's locale which doesn't match the server.
531 + - External changing data without sending a snapshot of it along with the HTML.
532 + - Invalid HTML tag nesting.
533 +
534 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
535 +
536 + https://react.dev/link/hydration-mismatch
537 + ]",
538 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
539 + ]
540 + `);
541 });
542
543 // @gate __DEV__
@@ -437,15 +552,23 @@ describe('ReactDOMServerHydration', () => {
552 );
553 }
554 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
440 - [
441 - "Warning: Expected server HTML to contain a matching text node for "third" in <div>.
442 - in div (at **)
443 - in Mismatch (at **)",
444 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
445 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
446 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
447 - ]
448 - `);
555 + [
556 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
557 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
558 +
559 + - A server/client branch \`if (typeof window !== 'undefined')\`.
560 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
561 + - Date formatting in a user's locale which doesn't match the server.
562 + - External changing data without sending a snapshot of it along with the HTML.
563 + - Invalid HTML tag nesting.
564 +
565 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
566 +
567 + https://react.dev/link/hydration-mismatch
568 + ]",
569 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
570 + ]
571 + `);
572 });
573 });
574 });
@@ -462,15 +585,23 @@ describe('ReactDOMServerHydration', () => {
585 );
586 }
587 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
465 - [
466 - "Warning: Did not expect server HTML to contain a <main> in <div>.
467 - in div (at **)
468 - in Mismatch (at **)",
469 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
470 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
471 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
472 - ]
473 - `);
588 + [
589 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
590 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
591 +
592 + - A server/client branch \`if (typeof window !== 'undefined')\`.
593 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
594 + - Date formatting in a user's locale which doesn't match the server.
595 + - External changing data without sending a snapshot of it along with the HTML.
596 + - Invalid HTML tag nesting.
597 +
598 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
599 +
600 + https://react.dev/link/hydration-mismatch
601 + ]",
602 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
603 + ]
604 + `);
605 });
606
607 // @gate __DEV__
@@ -486,12 +617,19 @@ describe('ReactDOMServerHydration', () => {
617 }
618 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
619 [
489 - "Warning: Expected server HTML to contain a matching <main> in <div>.
490 - in main (at **)
491 - in div (at **)
492 - in Mismatch (at **)",
620 "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
494 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
621 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
622 +
623 + - A server/client branch \`if (typeof window !== 'undefined')\`.
624 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
625 + - Date formatting in a user's locale which doesn't match the server.
626 + - External changing data without sending a snapshot of it along with the HTML.
627 + - Invalid HTML tag nesting.
628 +
629 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
630 +
631 + https://react.dev/link/hydration-mismatch
632 + ]",
633 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
634 ]
635 `);
@@ -509,16 +647,23 @@ describe('ReactDOMServerHydration', () => {
647 );
648 }
649 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
512 - [
513 - "Warning: Expected server HTML to contain a matching <footer> in <div>.
514 - in footer (at **)
515 - in div (at **)
516 - in Mismatch (at **)",
517 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
518 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
519 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
520 - ]
521 - `);
650 + [
651 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
652 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
653 +
654 + - A server/client branch \`if (typeof window !== 'undefined')\`.
655 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
656 + - Date formatting in a user's locale which doesn't match the server.
657 + - External changing data without sending a snapshot of it along with the HTML.
658 + - Invalid HTML tag nesting.
659 +
660 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
661 +
662 + https://react.dev/link/hydration-mismatch
663 + ]",
664 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
665 + ]
666 + `);
667 });
668
669 // @gate __DEV__
@@ -533,15 +678,23 @@ describe('ReactDOMServerHydration', () => {
678 );
679 }
680 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
536 - [
537 - "Warning: Did not expect server HTML to contain a <footer> in <div>.
538 - in div (at **)
539 - in Mismatch (at **)",
540 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
541 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
542 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
543 - ]
544 - `);
681 + [
682 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
683 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
684 +
685 + - A server/client branch \`if (typeof window !== 'undefined')\`.
686 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
687 + - Date formatting in a user's locale which doesn't match the server.
688 + - External changing data without sending a snapshot of it along with the HTML.
689 + - Invalid HTML tag nesting.
690 +
691 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
692 +
693 + https://react.dev/link/hydration-mismatch
694 + ]",
695 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
696 + ]
697 + `);
698 });
699 });
700
@@ -552,15 +705,23 @@ describe('ReactDOMServerHydration', () => {
705 return <div className="parent">{!isClient && 'only'}</div>;
706 }
707 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
555 - [
556 - "Warning: Did not expect server HTML to contain the text node "only" in <div>.
557 - in div (at **)
558 - in Mismatch (at **)",
559 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
560 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
561 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
562 - ]
563 - `);
708 + [
709 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
710 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
711 +
712 + - A server/client branch \`if (typeof window !== 'undefined')\`.
713 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
714 + - Date formatting in a user's locale which doesn't match the server.
715 + - External changing data without sending a snapshot of it along with the HTML.
716 + - Invalid HTML tag nesting.
717 +
718 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
719 +
720 + https://react.dev/link/hydration-mismatch
721 + ]",
722 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
723 + ]
724 + `);
725 });
726
727 // @gate __DEV__
@@ -576,12 +737,19 @@ describe('ReactDOMServerHydration', () => {
737 }
738 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
739 [
579 - "Warning: Expected server HTML to contain a matching <main> in <div>.
580 - in main (at **)
581 - in div (at **)
582 - in Mismatch (at **)",
740 "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
584 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
741 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
742 +
743 + - A server/client branch \`if (typeof window !== 'undefined')\`.
744 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
745 + - Date formatting in a user's locale which doesn't match the server.
746 + - External changing data without sending a snapshot of it along with the HTML.
747 + - Invalid HTML tag nesting.
748 +
749 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
750 +
751 + https://react.dev/link/hydration-mismatch
752 + ]",
753 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
754 ]
755 `);
@@ -599,16 +767,23 @@ describe('ReactDOMServerHydration', () => {
767 );
768 }
769 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
602 - [
603 - "Warning: Expected server HTML to contain a matching <footer> in <div>.
604 - in footer (at **)
605 - in div (at **)
606 - in Mismatch (at **)",
607 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
608 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
609 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
610 - ]
611 - `);
770 + [
771 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
772 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
773 +
774 + - A server/client branch \`if (typeof window !== 'undefined')\`.
775 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
776 + - Date formatting in a user's locale which doesn't match the server.
777 + - External changing data without sending a snapshot of it along with the HTML.
778 + - Invalid HTML tag nesting.
779 +
780 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
781 +
782 + https://react.dev/link/hydration-mismatch
783 + ]",
784 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
785 + ]
786 + `);
787 });
788
789 // @gate __DEV__
@@ -623,15 +798,23 @@ describe('ReactDOMServerHydration', () => {
798 );
799 }
800 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
626 - [
627 - "Warning: Did not expect server HTML to contain the text node "third" in <div>.
628 - in div (at **)
629 - in Mismatch (at **)",
630 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
631 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
632 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
633 - ]
634 - `);
801 + [
802 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
803 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
804 +
805 + - A server/client branch \`if (typeof window !== 'undefined')\`.
806 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
807 + - Date formatting in a user's locale which doesn't match the server.
808 + - External changing data without sending a snapshot of it along with the HTML.
809 + - Invalid HTML tag nesting.
810 +
811 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
812 +
813 + https://react.dev/link/hydration-mismatch
814 + ]",
815 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
816 + ]
817 + `);
818 });
819 });
820 });
@@ -656,16 +839,23 @@ describe('ReactDOMServerHydration', () => {
839 );
840 }
841 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
659 - [
660 - "Warning: Expected server HTML to contain a matching <Suspense> in <div>.
661 - in Suspense (at **)
662 - in div (at **)
663 - in Mismatch (at **)",
664 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
665 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
666 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
667 - ]
668 - `);
842 + [
843 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
844 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
845 +
846 + - A server/client branch \`if (typeof window !== 'undefined')\`.
847 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
848 + - Date formatting in a user's locale which doesn't match the server.
849 + - External changing data without sending a snapshot of it along with the HTML.
850 + - Invalid HTML tag nesting.
851 +
852 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
853 +
854 + https://react.dev/link/hydration-mismatch
855 + ]",
856 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
857 + ]
858 + `);
859 });
860
861 // @gate __DEV__
@@ -682,15 +872,23 @@ describe('ReactDOMServerHydration', () => {
872 );
873 }
874 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
685 - [
686 - "Warning: Did not expect server HTML to contain a <Suspense> in <div>.
687 - in div (at **)
688 - in Mismatch (at **)",
689 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
690 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
691 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
692 - ]
693 - `);
875 + [
876 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
877 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
878 +
879 + - A server/client branch \`if (typeof window !== 'undefined')\`.
880 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
881 + - Date formatting in a user's locale which doesn't match the server.
882 + - External changing data without sending a snapshot of it along with the HTML.
883 + - Invalid HTML tag nesting.
884 +
885 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
886 +
887 + https://react.dev/link/hydration-mismatch
888 + ]",
889 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
890 + ]
891 + `);
892 });
893
894 // @gate __DEV__
@@ -709,16 +907,23 @@ describe('ReactDOMServerHydration', () => {
907 }
908 // TODO: This message doesn't seem to have any useful details.
909 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
712 - [
713 - "Warning: Expected server HTML to contain a matching <Suspense> in <div>.
714 - in Suspense (at **)
715 - in div (at **)
716 - in Mismatch (at **)",
717 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
718 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
719 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
720 - ]
721 - `);
910 + [
911 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
912 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
913 +
914 + - A server/client branch \`if (typeof window !== 'undefined')\`.
915 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
916 + - Date formatting in a user's locale which doesn't match the server.
917 + - External changing data without sending a snapshot of it along with the HTML.
918 + - Invalid HTML tag nesting.
919 +
920 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
921 +
922 + https://react.dev/link/hydration-mismatch
923 + ]",
924 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
925 + ]
926 + `);
927 });
928
929 // @gate __DEV__
@@ -741,15 +946,23 @@ describe('ReactDOMServerHydration', () => {
946 // unhydrated tail nodes and this template is the first match. When we add special case handling for client
947 // rendered suspense boundaries this test will likely change again
948 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
744 - [
745 - "Warning: Did not expect server HTML to contain a <Suspense> in <div>.
746 - in div (at **)
747 - in Mismatch (at **)",
748 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
749 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
750 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
751 - ]
752 - `);
949 + [
950 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
951 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
952 +
953 + - A server/client branch \`if (typeof window !== 'undefined')\`.
954 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
955 + - Date formatting in a user's locale which doesn't match the server.
956 + - External changing data without sending a snapshot of it along with the HTML.
957 + - Invalid HTML tag nesting.
958 +
959 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
960 +
961 + https://react.dev/link/hydration-mismatch
962 + ]",
963 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
964 + ]
965 + `);
966 });
967
968 // @gate __DEV__
@@ -766,16 +979,22 @@ describe('ReactDOMServerHydration', () => {
979 );
980 }
981 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
769 - [
770 - "Warning: Expected server HTML to contain a matching <main> in <Suspense>.
771 - in main (at **)
772 - in Suspense (at **)
773 - in div (at **)
774 - in Mismatch (at **)",
775 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
776 - "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
777 - ]
778 - `);
982 + [
983 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
984 +
985 + - A server/client branch \`if (typeof window !== 'undefined')\`.
986 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
987 + - Date formatting in a user's locale which doesn't match the server.
988 + - External changing data without sending a snapshot of it along with the HTML.
989 + - Invalid HTML tag nesting.
990 +
991 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
992 +
993 + https://react.dev/link/hydration-mismatch
994 + ]",
995 + "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
996 + ]
997 + `);
998 });
999
1000 // @gate __DEV__
@@ -792,16 +1011,22 @@ describe('ReactDOMServerHydration', () => {
1011 );
1012 }
1013 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
795 - [
796 - "Warning: Expected server HTML to contain a matching <footer> in <Suspense>.
797 - in footer (at **)
798 - in Suspense (at **)
799 - in div (at **)
800 - in Mismatch (at **)",
801 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
802 - "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
803 - ]
804 - `);
1014 + [
1015 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
1016 +
1017 + - A server/client branch \`if (typeof window !== 'undefined')\`.
1018 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
1019 + - Date formatting in a user's locale which doesn't match the server.
1020 + - External changing data without sending a snapshot of it along with the HTML.
1021 + - Invalid HTML tag nesting.
1022 +
1023 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
1024 +
1025 + https://react.dev/link/hydration-mismatch
1026 + ]",
1027 + "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
1028 + ]
1029 + `);
1030 });
1031
1032 // @gate __DEV__
@@ -875,12 +1100,19 @@ describe('ReactDOMServerHydration', () => {
1100 }
1101 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1102 [
878 - "Warning: Expected server HTML to contain a matching <header> in <div>.
879 - in header (at **)
880 - in div (at **)
881 - in Mismatch (at **)",
1103 "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
883 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1104 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
1105 +
1106 + - A server/client branch \`if (typeof window !== 'undefined')\`.
1107 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
1108 + - Date formatting in a user's locale which doesn't match the server.
1109 + - External changing data without sending a snapshot of it along with the HTML.
1110 + - Invalid HTML tag nesting.
1111 +
1112 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
1113 +
1114 + https://react.dev/link/hydration-mismatch
1115 + ]",
1116 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1117 ]
1118 `);
@@ -902,15 +1134,23 @@ describe('ReactDOMServerHydration', () => {
1134 );
1135 }
1136 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
905 - [
906 - "Warning: Did not expect server HTML to contain a <header> in <div>.
907 - in div (at **)
908 - in Mismatch (at **)",
909 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
910 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
911 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
912 - ]
913 - `);
1137 + [
1138 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
1139 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
1140 +
1141 + - A server/client branch \`if (typeof window !== 'undefined')\`.
1142 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
1143 + - Date formatting in a user's locale which doesn't match the server.
1144 + - External changing data without sending a snapshot of it along with the HTML.
1145 + - Invalid HTML tag nesting.
1146 +
1147 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
1148 +
1149 + https://react.dev/link/hydration-mismatch
1150 + ]",
1151 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1152 + ]
1153 + `);
1154 });
1155 });
1156 });
@@ -951,18 +1191,23 @@ describe('ReactDOMServerHydration', () => {
1191 }
1192
1193 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
954 - [
955 - "Warning: Expected server HTML to contain a matching <footer> in <div>.
956 - in footer (at **)
957 - in Panel (at **)
958 - in div (at **)
959 - in ProfileSettings (at **)
960 - in Mismatch (at **)",
961 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
962 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
963 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
964 - ]
965 - `);
1194 + [
1195 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
1196 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
1197 +
1198 + - A server/client branch \`if (typeof window !== 'undefined')\`.
1199 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
1200 + - Date formatting in a user's locale which doesn't match the server.
1201 + - External changing data without sending a snapshot of it along with the HTML.
1202 + - Invalid HTML tag nesting.
1203 +
1204 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
1205 +
1206 + https://react.dev/link/hydration-mismatch
1207 + ]",
1208 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1209 + ]
1210 + `);
1211 });
1212
1213 // @gate __DEV__
@@ -1000,16 +1245,23 @@ describe('ReactDOMServerHydration', () => {
1245 }
1246
1247 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1003 - [
1004 - "Warning: Did not expect server HTML to contain a <footer> in <div>.
1005 - in div (at **)
1006 - in ProfileSettings (at **)
1007 - in Mismatch (at **)",
1008 - "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
1009 - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1010 - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1011 - ]
1012 - `);
1248 + [
1249 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
1250 + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
1251 +
1252 + - A server/client branch \`if (typeof window !== 'undefined')\`.
1253 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
1254 + - Date formatting in a user's locale which doesn't match the server.
1255 + - External changing data without sending a snapshot of it along with the HTML.
1256 + - Invalid HTML tag nesting.
1257 +
1258 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
1259 +
1260 + https://react.dev/link/hydration-mismatch
1261 + ]",
1262 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1263 + ]
1264 + `);
1265 });
1266 });
1267 });
packages/react-dom/src/__tests__/ReactDOMOption-test.js
-1
@@ -268,7 +268,6 @@ describe('ReactDOMOption', () => {
268 });
269 }).toErrorDev(
270 [
271 - 'Warning: Text content did not match. Server: "FooBaz" Client: "Foo"',
271 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
272 'Warning: In HTML, <div> cannot be a child of <option>',
273 ],
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+2 -1
@@ -171,7 +171,8 @@ describe('ReactDOMRoot', () => {
171 </div>,
172 );
173 await expect(async () => await waitForAll([])).toErrorDev(
174 - 'Extra attribute',
174 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
175 + {withoutStack: true},
176 );
177 });
178
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+368 -617
@@ -27,13 +27,13 @@ let waitFor;
27 let waitForPaint;
28 let assertLog;
29
30 -function normalizeCodeLocInfo(strOrErr) {
31 - if (strOrErr && strOrErr.replace) {
32 - return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
33 - return '\n in ' + name + ' (at **)';
34 - });
30 +function normalizeError(msg) {
31 + // Take the first sentence to make it easier to assert on.
32 + const idx = msg.indexOf('.');
33 + if (idx > -1) {
34 + return msg.slice(0, idx + 1);
35 }
36 - return strOrErr;
36 + return msg;
37 }
38
39 function dispatchMouseEvent(to, from) {
@@ -234,7 +234,7 @@ describe('ReactDOMServerPartialHydration', () => {
234 suspend = true;
235 ReactDOMClient.hydrateRoot(container, <App />, {
236 onRecoverableError(error) {
237 - Scheduler.log(error.message);
237 + Scheduler.log(normalizeError(error.message));
238 },
239 });
240 await waitForAll([]);
@@ -252,12 +252,6 @@ describe('ReactDOMServerPartialHydration', () => {
252 });
253
254 it('falls back to client rendering boundary on mismatch', async () => {
255 - // We can't use the toErrorDev helper here because this is async.
256 - const originalConsoleError = console.error;
257 - const mockError = jest.fn();
258 - console.error = (...args) => {
259 - mockError(...args.map(normalizeCodeLocInfo));
260 - };
255 let client = false;
256 let suspend = false;
257 let resolve;
@@ -294,85 +288,61 @@ describe('ReactDOMServerPartialHydration', () => {
288 </Suspense>
289 );
290 }
297 - try {
298 - const finalHTML = ReactDOMServer.renderToString(<App />);
299 - const container = document.createElement('section');
300 - container.innerHTML = finalHTML;
301 - assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
302 -
303 - expect(container.innerHTML).toBe(
304 - '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
305 - );
291 + const finalHTML = ReactDOMServer.renderToString(<App />);
292 + const container = document.createElement('section');
293 + container.innerHTML = finalHTML;
294 + assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
295
307 - suspend = true;
308 - client = true;
296 + expect(container.innerHTML).toBe(
297 + '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
298 + );
299
310 - ReactDOMClient.hydrateRoot(container, <App />, {
311 - onRecoverableError(error) {
312 - Scheduler.log(error.message);
313 - },
314 - });
315 - await waitForAll(['Suspend']);
316 - jest.runAllTimers();
300 + suspend = true;
301 + client = true;
302
318 - // Unchanged
319 - expect(container.innerHTML).toBe(
320 - '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
321 - );
303 + ReactDOMClient.hydrateRoot(container, <App />, {
304 + onRecoverableError(error) {
305 + Scheduler.log(normalizeError(error.message));
306 + },
307 + });
308 + await waitForAll(['Suspend']);
309 + jest.runAllTimers();
310
323 - suspend = false;
324 - resolve();
325 - await promise;
326 - await waitForAll([
327 - // first pass, mismatches at end
328 - 'Hello',
329 - 'Component',
330 - 'Component',
331 - 'Component',
332 - 'Component',
333 -
334 - // second pass as client render
335 - 'Hello',
336 - 'Component',
337 - 'Component',
338 - 'Component',
339 - 'Component',
340 -
341 - // Hydration mismatch is logged
342 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
343 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
344 - ]);
311 + // Unchanged
312 + expect(container.innerHTML).toBe(
313 + '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
314 + );
315
346 - // Client rendered - suspense comment nodes removed
347 - expect(container.innerHTML).toBe(
348 - 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
349 - );
316 + suspend = false;
317 + resolve();
318 + await promise;
319 + await waitForAll([
320 + // first pass, mismatches at end
321 + 'Hello',
322 + 'Component',
323 + 'Component',
324 + 'Component',
325 + 'Component',
326 +
327 + // second pass as client render
328 + 'Hello',
329 + 'Component',
330 + 'Component',
331 + 'Component',
332 + 'Component',
333 +
334 + // Hydration mismatch is logged
335 + "Hydration failed because the server rendered HTML didn't match the client.",
336 + 'There was an error while hydrating this Suspense boundary.',
337 + ]);
338
351 - if (__DEV__) {
352 - const lastCall = mockError.mock.calls[mockError.mock.calls.length - 1];
353 - expect(lastCall).toEqual([
354 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
355 - 'article',
356 - 'Suspense',
357 - '\n' +
358 - ' in article (at **)\n' +
359 - ' in Component (at **)\n' +
360 - ' in Suspense (at **)\n' +
361 - ' in App (at **)',
362 - ]);
363 - }
364 - } finally {
365 - console.error = originalConsoleError;
366 - }
339 + // Client rendered - suspense comment nodes removed
340 + expect(container.innerHTML).toBe(
341 + 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
342 + );
343 });
344
345 it('does not show a fallback if mismatch is after suspending', async () => {
370 - // We can't use the toErrorDev helper here because this is async.
371 - const originalConsoleError = console.error;
372 - const mockError = jest.fn();
373 - console.error = (...args) => {
374 - mockError(...args.map(normalizeCodeLocInfo));
375 - };
346 let client = false;
347 let suspend = false;
348 let resolve;
@@ -410,74 +380,50 @@ describe('ReactDOMServerPartialHydration', () => {
380 </Suspense>
381 );
382 }
413 - try {
414 - const finalHTML = ReactDOMServer.renderToString(<App />);
415 - const container = document.createElement('section');
416 - container.innerHTML = finalHTML;
417 - assertLog(['Hello', 'Component']);
383 + const finalHTML = ReactDOMServer.renderToString(<App />);
384 + const container = document.createElement('section');
385 + container.innerHTML = finalHTML;
386 + assertLog(['Hello', 'Component']);
387
419 - expect(container.innerHTML).toBe(
420 - '<!--$-->Hello<div>Component</div><!--/$-->',
421 - );
388 + expect(container.innerHTML).toBe(
389 + '<!--$-->Hello<div>Component</div><!--/$-->',
390 + );
391
423 - suspend = true;
424 - client = true;
392 + suspend = true;
393 + client = true;
394
426 - ReactDOMClient.hydrateRoot(container, <App />, {
427 - onRecoverableError(error) {
428 - Scheduler.log(error.message);
429 - },
430 - });
431 - await waitForAll(['Suspend']);
432 - jest.runAllTimers();
395 + ReactDOMClient.hydrateRoot(container, <App />, {
396 + onRecoverableError(error) {
397 + Scheduler.log(normalizeError(error.message));
398 + },
399 + });
400 + await waitForAll(['Suspend']);
401 + jest.runAllTimers();
402
434 - // !! Unchanged, continue showing server content while suspended.
435 - expect(container.innerHTML).toBe(
436 - '<!--$-->Hello<div>Component</div><!--/$-->',
437 - );
403 + // !! Unchanged, continue showing server content while suspended.
404 + expect(container.innerHTML).toBe(
405 + '<!--$-->Hello<div>Component</div><!--/$-->',
406 + );
407
439 - suspend = false;
440 - resolve();
441 - await promise;
442 - await waitForAll([
443 - // first pass, mismatches at end
444 - 'Hello',
445 - 'Component',
446 - 'Hello',
447 - 'Component',
448 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
449 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
450 - ]);
451 - jest.runAllTimers();
452 -
453 - // Client rendered - suspense comment nodes removed.
454 - expect(container.innerHTML).toBe('Hello<article>Mismatch</article>');
455 - if (__DEV__) {
456 - expect(mockError.mock.calls).toEqual([
457 - [
458 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
459 - 'article',
460 - 'Suspense',
461 - '\n' +
462 - ' in article (at **)\n' +
463 - ' in Component (at **)\n' +
464 - ' in Suspense (at **)\n' +
465 - ' in App (at **)',
466 - ],
467 - ]);
468 - }
469 - } finally {
470 - console.error = originalConsoleError;
471 - }
408 + suspend = false;
409 + resolve();
410 + await promise;
411 + await waitForAll([
412 + // first pass, mismatches at end
413 + 'Hello',
414 + 'Component',
415 + 'Hello',
416 + 'Component',
417 + "Hydration failed because the server rendered HTML didn't match the client.",
418 + 'There was an error while hydrating this Suspense boundary.',
419 + ]);
420 + jest.runAllTimers();
421 +
422 + // Client rendered - suspense comment nodes removed.
423 + expect(container.innerHTML).toBe('Hello<article>Mismatch</article>');
424 });
425
426 it('does not show a fallback if mismatch is child of suspended component', async () => {
475 - // We can't use the toErrorDev helper here because this is async.
476 - const originalConsoleError = console.error;
477 - const mockError = jest.fn();
478 - console.error = (...args) => {
479 - mockError(...args.map(normalizeCodeLocInfo));
480 - };
427 let client = false;
428 let suspend = false;
429 let resolve;
@@ -516,78 +462,50 @@ describe('ReactDOMServerPartialHydration', () => {
462 </Suspense>
463 );
464 }
519 - try {
520 - const finalHTML = ReactDOMServer.renderToString(<App />);
521 - const container = document.createElement('section');
522 - container.innerHTML = finalHTML;
523 - assertLog(['Hello', 'Component']);
465 + const finalHTML = ReactDOMServer.renderToString(<App />);
466 + const container = document.createElement('section');
467 + container.innerHTML = finalHTML;
468 + assertLog(['Hello', 'Component']);
469
525 - expect(container.innerHTML).toBe(
526 - '<!--$--><div><div>Component</div></div><!--/$-->',
527 - );
470 + expect(container.innerHTML).toBe(
471 + '<!--$--><div><div>Component</div></div><!--/$-->',
472 + );
473
529 - suspend = true;
530 - client = true;
474 + suspend = true;
475 + client = true;
476
532 - ReactDOMClient.hydrateRoot(container, <App />, {
533 - onRecoverableError(error) {
534 - Scheduler.log(error.message);
535 - },
536 - });
537 - await waitForAll(['Suspend']);
538 - jest.runAllTimers();
477 + ReactDOMClient.hydrateRoot(container, <App />, {
478 + onRecoverableError(error) {
479 + Scheduler.log(normalizeError(error.message));
480 + },
481 + });
482 + await waitForAll(['Suspend']);
483 + jest.runAllTimers();
484
540 - // !! Unchanged, continue showing server content while suspended.
541 - expect(container.innerHTML).toBe(
542 - '<!--$--><div><div>Component</div></div><!--/$-->',
543 - );
485 + // !! Unchanged, continue showing server content while suspended.
486 + expect(container.innerHTML).toBe(
487 + '<!--$--><div><div>Component</div></div><!--/$-->',
488 + );
489
545 - suspend = false;
546 - resolve();
547 - await promise;
548 - await waitForAll([
549 - // first pass, mismatches at end
550 - 'Hello',
551 - 'Component',
552 - 'Hello',
553 - 'Component',
554 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
555 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
556 - ]);
557 - jest.runAllTimers();
490 + suspend = false;
491 + resolve();
492 + await promise;
493 + await waitForAll([
494 + // first pass, mismatches at end
495 + 'Hello',
496 + 'Component',
497 + 'Hello',
498 + 'Component',
499 + "Hydration failed because the server rendered HTML didn't match the client.",
500 + 'There was an error while hydrating this Suspense boundary.',
501 + ]);
502 + jest.runAllTimers();
503
559 - // Client rendered - suspense comment nodes removed
560 - expect(container.innerHTML).toBe(
561 - '<div><article>Mismatch</article></div>',
562 - );
563 - if (__DEV__) {
564 - expect(mockError.mock.calls).toEqual([
565 - [
566 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
567 - 'article',
568 - 'div',
569 - '\n' +
570 - ' in article (at **)\n' +
571 - ' in Component (at **)\n' +
572 - ' in div (at **)\n' +
573 - ' in Child (at **)\n' +
574 - ' in Suspense (at **)\n' +
575 - ' in App (at **)',
576 - ],
577 - ]);
578 - }
579 - } finally {
580 - console.error = originalConsoleError;
581 - }
504 + // Client rendered - suspense comment nodes removed
505 + expect(container.innerHTML).toBe('<div><article>Mismatch</article></div>');
506 });
507
508 it('does not show a fallback if mismatch is parent and first child suspends', async () => {
585 - // We can't use the toErrorDev helper here because this is async.
586 - const originalConsoleError = console.error;
587 - const mockError = jest.fn();
588 - console.error = (...args) => {
589 - mockError(...args.map(normalizeCodeLocInfo));
590 - };
509 let client = false;
510 let suspend = false;
511 let resolve;
@@ -636,77 +554,52 @@ describe('ReactDOMServerPartialHydration', () => {
554 </Suspense>
555 );
556 }
639 - try {
640 - const finalHTML = ReactDOMServer.renderToString(<App />);
641 - const container = document.createElement('section');
642 - container.innerHTML = finalHTML;
643 - assertLog(['Component', 'Hello']);
557 + const finalHTML = ReactDOMServer.renderToString(<App />);
558 + const container = document.createElement('section');
559 + container.innerHTML = finalHTML;
560 + assertLog(['Component', 'Hello']);
561
645 - expect(container.innerHTML).toBe(
646 - '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
647 - );
562 + expect(container.innerHTML).toBe(
563 + '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
564 + );
565
649 - suspend = true;
650 - client = true;
566 + suspend = true;
567 + client = true;
568
652 - ReactDOMClient.hydrateRoot(container, <App />, {
653 - onRecoverableError(error) {
654 - Scheduler.log(error.message);
655 - },
656 - });
657 - await waitForAll(['Component', 'Suspend']);
658 - jest.runAllTimers();
569 + ReactDOMClient.hydrateRoot(container, <App />, {
570 + onRecoverableError(error) {
571 + Scheduler.log(normalizeError(error.message));
572 + },
573 + });
574 + await waitForAll(['Component', 'Suspend']);
575 + jest.runAllTimers();
576
660 - // !! Unchanged, continue showing server content while suspended.
661 - expect(container.innerHTML).toBe(
662 - '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
663 - );
577 + // !! Unchanged, continue showing server content while suspended.
578 + expect(container.innerHTML).toBe(
579 + '<!--$--><div><div></div><div>Component</div></div><!--/$-->',
580 + );
581
665 - suspend = false;
666 - resolve();
667 - await promise;
668 - await waitForAll([
669 - // first pass, mismatches at end
670 - 'Component',
671 - 'Hello',
672 - 'Component',
673 - 'Hello',
674 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
675 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
676 - ]);
677 - jest.runAllTimers();
582 + suspend = false;
583 + resolve();
584 + await promise;
585 + await waitForAll([
586 + // first pass, mismatches at end
587 + 'Component',
588 + 'Hello',
589 + 'Component',
590 + 'Hello',
591 + "Hydration failed because the server rendered HTML didn't match the client.",
592 + 'There was an error while hydrating this Suspense boundary.',
593 + ]);
594 + jest.runAllTimers();
595
679 - // Client rendered - suspense comment nodes removed
680 - expect(container.innerHTML).toBe(
681 - '<div><div></div><article>Mismatch</article></div>',
682 - );
683 - if (__DEV__) {
684 - expect(mockError.mock.calls).toEqual([
685 - [
686 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
687 - 'article',
688 - 'div',
689 - '\n' +
690 - ' in article (at **)\n' +
691 - ' in div (at **)\n' +
692 - ' in Component (at **)\n' +
693 - ' in Suspense (at **)\n' +
694 - ' in App (at **)',
695 - ],
696 - ]);
697 - }
698 - } finally {
699 - console.error = originalConsoleError;
700 - }
596 + // Client rendered - suspense comment nodes removed
597 + expect(container.innerHTML).toBe(
598 + '<div><div></div><article>Mismatch</article></div>',
599 + );
600 });
601
602 it('does show a fallback if mismatch is parent and second child suspends', async () => {
704 - // We can't use the toErrorDev helper here because this is async.
705 - const originalConsoleError = console.error;
706 - const mockError = jest.fn();
707 - console.error = (...args) => {
708 - mockError(...args.map(normalizeCodeLocInfo));
709 - };
603 let client = false;
604 let suspend = false;
605 let resolve;
@@ -755,74 +648,49 @@ describe('ReactDOMServerPartialHydration', () => {
648 </Suspense>
649 );
650 }
758 - try {
759 - const finalHTML = ReactDOMServer.renderToString(<App />);
760 - const container = document.createElement('section');
761 - container.innerHTML = finalHTML;
762 - assertLog(['Component', 'Hello']);
651 + const finalHTML = ReactDOMServer.renderToString(<App />);
652 + const container = document.createElement('section');
653 + container.innerHTML = finalHTML;
654 + assertLog(['Component', 'Hello']);
655
764 - expect(container.innerHTML).toBe(
765 - '<!--$--><div><div>Component</div><div></div></div><!--/$-->',
766 - );
656 + expect(container.innerHTML).toBe(
657 + '<!--$--><div><div>Component</div><div></div></div><!--/$-->',
658 + );
659
768 - suspend = true;
769 - client = true;
660 + suspend = true;
661 + client = true;
662
771 - ReactDOMClient.hydrateRoot(container, <App />, {
772 - onRecoverableError(error) {
773 - Scheduler.log(error.message);
774 - },
775 - });
776 - await waitForAll([
777 - 'Component',
778 - 'Component',
779 - 'Suspend',
780 - 'Fallback',
781 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
782 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
783 - ]);
784 - jest.runAllTimers();
663 + ReactDOMClient.hydrateRoot(container, <App />, {
664 + onRecoverableError(error) {
665 + Scheduler.log(normalizeError(error.message));
666 + },
667 + });
668 + await waitForAll([
669 + 'Component',
670 + 'Component',
671 + 'Suspend',
672 + 'Fallback',
673 + "Hydration failed because the server rendered HTML didn't match the client.",
674 + 'There was an error while hydrating this Suspense boundary.',
675 + ]);
676 + jest.runAllTimers();
677
786 - // !! Client switches to suspense fallback.
787 - expect(container.innerHTML).toBe('Loading...');
678 + // !! Client switches to suspense fallback.
679 + expect(container.innerHTML).toBe('Loading...');
680
789 - suspend = false;
790 - resolve();
791 - await promise;
792 - await waitForAll(['Component', 'Hello']);
793 - jest.runAllTimers();
681 + suspend = false;
682 + resolve();
683 + await promise;
684 + await waitForAll(['Component', 'Hello']);
685 + jest.runAllTimers();
686
795 - // Client rendered - suspense comment nodes removed
796 - expect(container.innerHTML).toBe(
797 - '<div><article>Mismatch</article><div></div></div>',
798 - );
799 - if (__DEV__) {
800 - expect(mockError.mock.calls).toEqual([
801 - [
802 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
803 - 'article',
804 - 'div',
805 - '\n' +
806 - ' in article (at **)\n' +
807 - ' in div (at **)\n' +
808 - ' in Component (at **)\n' +
809 - ' in Suspense (at **)\n' +
810 - ' in App (at **)',
811 - ],
812 - ]);
813 - }
814 - } finally {
815 - console.error = originalConsoleError;
816 - }
687 + // Client rendered - suspense comment nodes removed
688 + expect(container.innerHTML).toBe(
689 + '<div><article>Mismatch</article><div></div></div>',
690 + );
691 });
692
693 it('does show a fallback if mismatch is in parent element only', async () => {
820 - // We can't use the toErrorDev helper here because this is async.
821 - const originalConsoleError = console.error;
822 - const mockError = jest.fn();
823 - console.error = (...args) => {
824 - mockError(...args.map(normalizeCodeLocInfo));
825 - };
694 let client = false;
695 let suspend = false;
696 let resolve;
@@ -861,71 +729,45 @@ describe('ReactDOMServerPartialHydration', () => {
729 </Suspense>
730 );
731 }
864 - try {
865 - const finalHTML = ReactDOMServer.renderToString(<App />);
866 - const container = document.createElement('section');
867 - container.innerHTML = finalHTML;
868 - assertLog(['Component', 'Hello']);
732 + const finalHTML = ReactDOMServer.renderToString(<App />);
733 + const container = document.createElement('section');
734 + container.innerHTML = finalHTML;
735 + assertLog(['Component', 'Hello']);
736
870 - expect(container.innerHTML).toBe(
871 - '<!--$--><div><div></div></div><!--/$-->',
872 - );
737 + expect(container.innerHTML).toBe('<!--$--><div><div></div></div><!--/$-->');
738
874 - suspend = true;
875 - client = true;
739 + suspend = true;
740 + client = true;
741
877 - ReactDOMClient.hydrateRoot(container, <App />, {
878 - onRecoverableError(error) {
879 - Scheduler.log(error.message);
880 - },
881 - });
882 - await waitForAll([
883 - 'Component',
884 - 'Component',
885 - 'Suspend',
886 - 'Fallback',
887 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
888 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
889 - ]);
890 - jest.runAllTimers();
742 + ReactDOMClient.hydrateRoot(container, <App />, {
743 + onRecoverableError(error) {
744 + Scheduler.log(normalizeError(error.message));
745 + },
746 + });
747 + await waitForAll([
748 + 'Component',
749 + 'Component',
750 + 'Suspend',
751 + 'Fallback',
752 + "Hydration failed because the server rendered HTML didn't match the client.",
753 + 'There was an error while hydrating this Suspense boundary.',
754 + ]);
755 + jest.runAllTimers();
756
892 - // !! Client switches to suspense fallback.
893 - expect(container.innerHTML).toBe('Loading...');
757 + // !! Client switches to suspense fallback.
758 + expect(container.innerHTML).toBe('Loading...');
759
895 - suspend = false;
896 - resolve();
897 - await promise;
898 - await waitForAll(['Component', 'Hello']);
899 - jest.runAllTimers();
900 -
901 - // Client rendered - suspense comment nodes removed
902 - expect(container.innerHTML).toBe('<article><div></div></article>');
903 - if (__DEV__) {
904 - expect(mockError.mock.calls).toEqual([
905 - [
906 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
907 - 'article',
908 - 'Suspense',
909 - '\n' +
910 - ' in article (at **)\n' +
911 - ' in Component (at **)\n' +
912 - ' in Suspense (at **)\n' +
913 - ' in App (at **)',
914 - ],
915 - ]);
916 - }
917 - } finally {
918 - console.error = originalConsoleError;
919 - }
760 + suspend = false;
761 + resolve();
762 + await promise;
763 + await waitForAll(['Component', 'Hello']);
764 + jest.runAllTimers();
765 +
766 + // Client rendered - suspense comment nodes removed
767 + expect(container.innerHTML).toBe('<article><div></div></article>');
768 });
769
770 it('does show a fallback if mismatch is before suspending', async () => {
923 - // We can't use the toErrorDev helper here because this is async.
924 - const originalConsoleError = console.error;
925 - const mockError = jest.fn();
926 - console.error = (...args) => {
927 - mockError(...args.map(normalizeCodeLocInfo));
928 - };
771 let client = false;
772 let suspend = false;
773 let resolve;
@@ -963,75 +805,51 @@ describe('ReactDOMServerPartialHydration', () => {
805 </Suspense>
806 );
807 }
966 - try {
967 - const finalHTML = ReactDOMServer.renderToString(<App />);
968 - const container = document.createElement('section');
969 - container.innerHTML = finalHTML;
970 - assertLog(['Component', 'Hello']);
808 + const finalHTML = ReactDOMServer.renderToString(<App />);
809 + const container = document.createElement('section');
810 + container.innerHTML = finalHTML;
811 + assertLog(['Component', 'Hello']);
812
972 - expect(container.innerHTML).toBe(
973 - '<!--$--><div>Component</div>Hello<!--/$-->',
974 - );
813 + expect(container.innerHTML).toBe(
814 + '<!--$--><div>Component</div>Hello<!--/$-->',
815 + );
816
976 - suspend = true;
977 - client = true;
817 + suspend = true;
818 + client = true;
819
979 - ReactDOMClient.hydrateRoot(container, <App />, {
980 - onRecoverableError(error) {
981 - Scheduler.log(error.message);
982 - },
983 - });
984 - await waitForAll([
985 - 'Component',
986 - 'Component',
987 - 'Suspend',
988 - 'Fallback',
989 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
990 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
991 - ]);
992 - jest.runAllTimers();
820 + ReactDOMClient.hydrateRoot(container, <App />, {
821 + onRecoverableError(error) {
822 + Scheduler.log(normalizeError(error.message));
823 + },
824 + });
825 + await waitForAll([
826 + 'Component',
827 + 'Component',
828 + 'Suspend',
829 + 'Fallback',
830 + "Hydration failed because the server rendered HTML didn't match the client.",
831 + 'There was an error while hydrating this Suspense boundary.',
832 + ]);
833 + jest.runAllTimers();
834
994 - // !! Client switches to suspense fallback.
995 - expect(container.innerHTML).toBe('Loading...');
835 + // !! Client switches to suspense fallback.
836 + expect(container.innerHTML).toBe('Loading...');
837
997 - suspend = false;
998 - resolve();
999 - await promise;
1000 - await waitForAll([
1001 - // first pass, mismatches at end
1002 - 'Component',
1003 - 'Hello',
1004 - ]);
1005 - jest.runAllTimers();
1006 -
1007 - // Client rendered - suspense comment nodes removed
1008 - expect(container.innerHTML).toBe('<article>Mismatch</article>Hello');
1009 - if (__DEV__) {
1010 - expect(mockError.mock.calls).toEqual([
1011 - [
1012 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1013 - 'article',
1014 - 'Suspense',
1015 - '\n' +
1016 - ' in article (at **)\n' +
1017 - ' in Component (at **)\n' +
1018 - ' in Suspense (at **)\n' +
1019 - ' in App (at **)',
1020 - ],
1021 - ]);
1022 - }
1023 - } finally {
1024 - console.error = originalConsoleError;
1025 - }
838 + suspend = false;
839 + resolve();
840 + await promise;
841 + await waitForAll([
842 + // first pass, mismatches at end
843 + 'Component',
844 + 'Hello',
845 + ]);
846 + jest.runAllTimers();
847 +
848 + // Client rendered - suspense comment nodes removed
849 + expect(container.innerHTML).toBe('<article>Mismatch</article>Hello');
850 });
851
852 it('does show a fallback if mismatch is before suspending in a child', async () => {
1029 - // We can't use the toErrorDev helper here because this is async.
1030 - const originalConsoleError = console.error;
1031 - const mockError = jest.fn();
1032 - console.error = (...args) => {
1033 - mockError(...args.map(normalizeCodeLocInfo));
1034 - };
853 let client = false;
854 let suspend = false;
855 let resolve;
@@ -1071,68 +889,50 @@ describe('ReactDOMServerPartialHydration', () => {
889 </Suspense>
890 );
891 }
1074 - try {
1075 - const finalHTML = ReactDOMServer.renderToString(<App />);
1076 - const container = document.createElement('section');
1077 - container.innerHTML = finalHTML;
1078 - assertLog(['Component', 'Hello']);
892 + const finalHTML = ReactDOMServer.renderToString(<App />);
893 + const container = document.createElement('section');
894 + container.innerHTML = finalHTML;
895 + assertLog(['Component', 'Hello']);
896
1080 - expect(container.innerHTML).toBe(
1081 - '<!--$--><div>Component</div><div>Hello</div><!--/$-->',
1082 - );
897 + expect(container.innerHTML).toBe(
898 + '<!--$--><div>Component</div><div>Hello</div><!--/$-->',
899 + );
900
1084 - suspend = true;
1085 - client = true;
901 + suspend = true;
902 + client = true;
903
1087 - ReactDOMClient.hydrateRoot(container, <App />, {
1088 - onRecoverableError(error) {
1089 - Scheduler.log(error.message);
1090 - },
1091 - });
1092 - await waitForAll([
1093 - 'Component',
1094 - 'Component',
1095 - 'Suspend',
1096 - 'Fallback',
1097 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
1098 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
1099 - ]);
1100 - jest.runAllTimers();
904 + ReactDOMClient.hydrateRoot(container, <App />, {
905 + onRecoverableError(error) {
906 + Scheduler.log(normalizeError(error.message));
907 + },
908 + });
909 + await waitForAll([
910 + 'Component',
911 + 'Component',
912 + 'Suspend',
913 + 'Fallback',
914 + "Hydration failed because the server rendered HTML didn't match the client.",
915 + 'There was an error while hydrating this Suspense boundary.',
916 + ]);
917 + jest.runAllTimers();
918
1102 - // !! Client switches to suspense fallback.
1103 - expect(container.innerHTML).toBe('Loading...');
919 + // !! Client switches to suspense fallback.
920 + expect(container.innerHTML).toBe('Loading...');
921
1105 - suspend = false;
1106 - resolve();
1107 - await promise;
1108 - await waitForAll([
1109 - // first pass, mismatches at end
1110 - 'Component',
1111 - 'Hello',
1112 - ]);
1113 - jest.runAllTimers();
922 + suspend = false;
923 + resolve();
924 + await promise;
925 + await waitForAll([
926 + // first pass, mismatches at end
927 + 'Component',
928 + 'Hello',
929 + ]);
930 + jest.runAllTimers();
931
1115 - // Client rendered - suspense comment nodes removed.
1116 - expect(container.innerHTML).toBe(
1117 - '<article>Mismatch</article><div>Hello</div>',
1118 - );
1119 - if (__DEV__) {
1120 - expect(mockError.mock.calls).toEqual([
1121 - [
1122 - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1123 - 'article',
1124 - 'Suspense',
1125 - '\n' +
1126 - ' in article (at **)\n' +
1127 - ' in Component (at **)\n' +
1128 - ' in Suspense (at **)\n' +
1129 - ' in App (at **)',
1130 - ],
1131 - ]);
1132 - }
1133 - } finally {
1134 - console.error = originalConsoleError;
1135 - }
932 + // Client rendered - suspense comment nodes removed.
933 + expect(container.innerHTML).toBe(
934 + '<article>Mismatch</article><div>Hello</div>',
935 + );
936 });
937
938 it('calls the hydration callbacks after hydration or deletion', async () => {
@@ -1195,7 +995,7 @@ describe('ReactDOMServerPartialHydration', () => {
995 deleted.push(node);
996 },
997 onRecoverableError(error) {
1198 - Scheduler.log(error.message);
998 + Scheduler.log(normalizeError(error.message));
999 },
1000 });
1001 await waitForAll([]);
@@ -1290,17 +1090,13 @@ describe('ReactDOMServerPartialHydration', () => {
1090 expect(container.innerHTML).toContain('<span>B</span>');
1091 expect(ref.current).toBe(null);
1092
1293 - await expect(async () => {
1294 - await act(() => {
1295 - ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1296 - onRecoverableError(error) {
1297 - Scheduler.log(error.message);
1298 - },
1299 - });
1093 + await act(() => {
1094 + ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1095 + onRecoverableError(error) {
1096 + Scheduler.log(normalizeError(error.message));
1097 + },
1098 });
1301 - }).toErrorDev(
1302 - 'Did not expect server HTML to contain a <span> in <Suspense>',
1303 - );
1099 + });
1100
1101 expect(container.innerHTML).toContain('<span>A</span>');
1102 expect(container.innerHTML).not.toContain('<span>B</span>');
@@ -1308,21 +1104,13 @@ describe('ReactDOMServerPartialHydration', () => {
1104 assertLog([
1105 'Server rendered',
1106 'Client rendered',
1311 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
1312 - 'There was an error while hydrating this Suspense boundary. ' +
1313 - 'Switched to client rendering.',
1107 + "Hydration failed because the server rendered HTML didn't match the client.",
1108 + 'There was an error while hydrating this Suspense boundary.',
1109 ]);
1110 expect(ref.current).not.toBe(span);
1111 });
1112
1113 it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
1319 - // We can't use the toErrorDev helper here because this is async.
1320 - const originalConsoleError = console.error;
1321 - const mockError = jest.fn();
1322 - console.error = (...args) => {
1323 - mockError(...args.map(normalizeCodeLocInfo));
1324 - };
1325 -
1114 const ref = React.createRef();
1115 let shouldSuspend = false;
1116 let resolve;
@@ -1350,44 +1138,34 @@ describe('ReactDOMServerPartialHydration', () => {
1138 </div>
1139 );
1140 }
1353 - try {
1354 - const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
1141 + const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
1142
1356 - const container = document.createElement('div');
1357 - container.innerHTML = finalHTML;
1143 + const container = document.createElement('div');
1144 + container.innerHTML = finalHTML;
1145
1359 - const span = container.getElementsByTagName('span')[0];
1146 + const span = container.getElementsByTagName('span')[0];
1147
1361 - expect(container.innerHTML).toContain('<span>A</span>');
1362 - expect(container.innerHTML).toContain('<span>B</span>');
1363 - expect(ref.current).toBe(null);
1148 + expect(container.innerHTML).toContain('<span>A</span>');
1149 + expect(container.innerHTML).toContain('<span>B</span>');
1150 + expect(ref.current).toBe(null);
1151
1365 - shouldSuspend = true;
1366 - await act(() => {
1367 - ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
1368 - });
1152 + shouldSuspend = true;
1153 + await act(() => {
1154 + ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
1155 + });
1156
1157 + await expect(async () => {
1158 await act(() => {
1159 resolve();
1160 });
1161 + }).toErrorDev([
1162 + "Hydration failed because the server rendered HTML didn't match the client.",
1163 + 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
1164 + ]);
1165
1374 - expect(container.innerHTML).toContain('<span>A</span>');
1375 - expect(container.innerHTML).not.toContain('<span>B</span>');
1376 - expect(ref.current).not.toBe(span);
1377 - if (__DEV__) {
1378 - expect(mockError).toHaveBeenCalledWith(
1379 - 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
1380 - 'span',
1381 - 'Suspense',
1382 - '\n' +
1383 - ' in Suspense (at **)\n' +
1384 - ' in div (at **)\n' +
1385 - ' in App (at **)',
1386 - );
1387 - }
1388 - } finally {
1389 - console.error = originalConsoleError;
1390 - }
1166 + expect(container.innerHTML).toContain('<span>A</span>');
1167 + expect(container.innerHTML).not.toContain('<span>B</span>');
1168 + expect(ref.current).not.toBe(span);
1169 });
1170
1171 it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {
@@ -1417,18 +1195,16 @@ describe('ReactDOMServerPartialHydration', () => {
1195 expect(container.innerHTML).toContain('<span>B</span>');
1196 expect(ref.current).toBe(null);
1197
1420 - await expect(async () => {
1421 - await act(() => {
1422 - ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1423 - onRecoverableError(error) {
1424 - Scheduler.log(error.message);
1425 - },
1426 - });
1198 + await act(() => {
1199 + ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
1200 + onRecoverableError(error) {
1201 + Scheduler.log(normalizeError(error.message));
1202 + },
1203 });
1428 - }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
1204 + });
1205 assertLog([
1430 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
1431 - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
1206 + "Hydration failed because the server rendered HTML didn't match the client.",
1207 + 'There was an error while hydrating this Suspense boundary.',
1208 ]);
1209
1210 expect(container.innerHTML).toContain('<span>A</span>');
@@ -1787,7 +1563,7 @@ describe('ReactDOMServerPartialHydration', () => {
1563 <App text="Hello" className="hello" />,
1564 {
1565 onRecoverableError(error) {
1790 - Scheduler.log(error.message);
1566 + Scheduler.log(normalizeError(error.message));
1567 },
1568 },
1569 );
@@ -1865,7 +1641,7 @@ describe('ReactDOMServerPartialHydration', () => {
1641 <App text="Hello" className="hello" />,
1642 {
1643 onRecoverableError(error) {
1868 - Scheduler.log(error.message);
1644 + Scheduler.log(normalizeError(error.message));
1645 },
1646 },
1647 );
@@ -1941,7 +1717,7 @@ describe('ReactDOMServerPartialHydration', () => {
1717 <App text="Hello" className="hello" />,
1718 {
1719 onRecoverableError(error) {
1944 - Scheduler.log(error.message);
1720 + Scheduler.log(normalizeError(error.message));
1721 },
1722 },
1723 );
@@ -2249,7 +2025,7 @@ describe('ReactDOMServerPartialHydration', () => {
2025 </Context.Provider>,
2026 {
2027 onRecoverableError(error) {
2252 - Scheduler.log(error.message);
2028 + Scheduler.log(normalizeError(error.message));
2029 },
2030 },
2031 );
@@ -2324,22 +2100,18 @@ describe('ReactDOMServerPartialHydration', () => {
2100 suspend = false;
2101 ReactDOMClient.hydrateRoot(container, <App />, {
2102 onRecoverableError(error) {
2327 - Scheduler.log(error.message);
2103 + Scheduler.log(normalizeError(error.message));
2104 },
2105 });
2106 if (__DEV__) {
2107 await waitForAll([
2108 'The server did not finish this Suspense boundary: The server used' +
2333 - ' "renderToString" which does not support Suspense. If you intended' +
2334 - ' for this Suspense boundary to render the fallback content on the' +
2335 - ' server consider throwing an Error somewhere within the Suspense boundary.' +
2336 - ' If you intended to have the server wait for the suspended component' +
2337 - ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2109 + ' "renderToString" which does not support Suspense.',
2110 ]);
2111 } else {
2112 await waitForAll([
2113 'The server could not finish this Suspense boundary, likely due to ' +
2342 - 'an error during server rendering. Switched to client rendering.',
2114 + 'an error during server rendering.',
2115 ]);
2116 }
2117 jest.runAllTimers();
@@ -2397,22 +2169,18 @@ describe('ReactDOMServerPartialHydration', () => {
2169 suspend = false;
2170 ReactDOMClient.hydrateRoot(container, <App />, {
2171 onRecoverableError(error) {
2400 - Scheduler.log(error.message);
2172 + Scheduler.log(normalizeError(error.message));
2173 },
2174 });
2175 if (__DEV__) {
2176 await waitForAll([
2177 'The server did not finish this Suspense boundary: The server used' +
2406 - ' "renderToString" which does not support Suspense. If you intended' +
2407 - ' for this Suspense boundary to render the fallback content on the' +
2408 - ' server consider throwing an Error somewhere within the Suspense boundary.' +
2409 - ' If you intended to have the server wait for the suspended component' +
2410 - ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2178 + ' "renderToString" which does not support Suspense.',
2179 ]);
2180 } else {
2181 await waitForAll([
2182 'The server could not finish this Suspense boundary, likely due to ' +
2415 - 'an error during server rendering. Switched to client rendering.',
2183 + 'an error during server rendering.',
2184 ]);
2185 }
2186 // This will have exceeded the suspended time so we should timeout.
@@ -2475,22 +2243,18 @@ describe('ReactDOMServerPartialHydration', () => {
2243 suspend = false;
2244 ReactDOMClient.hydrateRoot(container, <App />, {
2245 onRecoverableError(error) {
2478 - Scheduler.log(error.message);
2246 + Scheduler.log(normalizeError(error.message));
2247 },
2248 });
2249 if (__DEV__) {
2250 await waitForAll([
2251 'The server did not finish this Suspense boundary: The server used' +
2484 - ' "renderToString" which does not support Suspense. If you intended' +
2485 - ' for this Suspense boundary to render the fallback content on the' +
2486 - ' server consider throwing an Error somewhere within the Suspense boundary.' +
2487 - ' If you intended to have the server wait for the suspended component' +
2488 - ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2252 + ' "renderToString" which does not support Suspense.',
2253 ]);
2254 } else {
2255 await waitForAll([
2256 'The server could not finish this Suspense boundary, likely due to ' +
2493 - 'an error during server rendering. Switched to client rendering.',
2257 + 'an error during server rendering.',
2258 ]);
2259 }
2260 // This will have exceeded the suspended time so we should timeout.
@@ -2797,7 +2561,7 @@ describe('ReactDOMServerPartialHydration', () => {
2561
2562 ReactDOMClient.hydrateRoot(container, <App />, {
2563 onRecoverableError(error) {
2800 - Scheduler.log(error.message);
2564 + Scheduler.log(normalizeError(error.message));
2565 },
2566 });
2567
@@ -2805,16 +2569,12 @@ describe('ReactDOMServerPartialHydration', () => {
2569 if (__DEV__) {
2570 await waitForAll([
2571 'The server did not finish this Suspense boundary: The server used' +
2808 - ' "renderToString" which does not support Suspense. If you intended' +
2809 - ' for this Suspense boundary to render the fallback content on the' +
2810 - ' server consider throwing an Error somewhere within the Suspense boundary.' +
2811 - ' If you intended to have the server wait for the suspended component' +
2812 - ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2572 + ' "renderToString" which does not support Suspense.',
2573 ]);
2574 } else {
2575 await waitForAll([
2576 'The server could not finish this Suspense boundary, likely due to ' +
2817 - 'an error during server rendering. Switched to client rendering.',
2577 + 'an error during server rendering.',
2578 ]);
2579 }
2580
@@ -2873,22 +2633,18 @@ describe('ReactDOMServerPartialHydration', () => {
2633 suspend = false;
2634 ReactDOMClient.hydrateRoot(container, <App />, {
2635 onRecoverableError(error) {
2876 - Scheduler.log(error.message);
2636 + Scheduler.log(normalizeError(error.message));
2637 },
2638 });
2639 if (__DEV__) {
2640 await waitForAll([
2641 'The server did not finish this Suspense boundary: The server used' +
2882 - ' "renderToString" which does not support Suspense. If you intended' +
2883 - ' for this Suspense boundary to render the fallback content on the' +
2884 - ' server consider throwing an Error somewhere within the Suspense boundary.' +
2885 - ' If you intended to have the server wait for the suspended component' +
2886 - ' please switch to "renderToPipeableStream" which supports Suspense on the server',
2642 + ' "renderToString" which does not support Suspense.',
2643 ]);
2644 } else {
2645 await waitForAll([
2646 'The server could not finish this Suspense boundary, likely due to ' +
2891 - 'an error during server rendering. Switched to client rendering.',
2647 + 'an error during server rendering.',
2648 ]);
2649 }
2650 jest.runAllTimers();
@@ -2989,7 +2745,7 @@ describe('ReactDOMServerPartialHydration', () => {
2745 </ClassName.Provider>,
2746 {
2747 onRecoverableError(error) {
2992 - Scheduler.log(error.message);
2748 + Scheduler.log(normalizeError(error.message));
2749 },
2750 },
2751 );
@@ -4027,7 +3783,9 @@ describe('ReactDOMServerPartialHydration', () => {
3783 await act(() => {
3784 ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
3785 onRecoverableError(error) {
4030 - Scheduler.log('Log recoverable error: ' + error.message);
3786 + Scheduler.log(
3787 + 'Log recoverable error: ' + normalizeError(error.message),
3788 + );
3789 },
3790 });
3791 });
@@ -4035,15 +3793,12 @@ describe('ReactDOMServerPartialHydration', () => {
3793 [
3794 'Warning: An error occurred during hydration. ' +
3795 'The server HTML was replaced with client content.',
4038 - 'Warning: Expected server HTML to contain a matching <span> in the root.\n' +
4039 - ' in span (at **)\n' +
4040 - ' in App (at **)',
3796 ],
3797 {withoutStack: 1},
3798 );
3799 assertLog([
4045 - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
4046 - 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
3800 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
3801 + 'Log recoverable error: There was an error while hydrating.',
3802 ]);
3803
3804 // We show fallback state when mismatch happens at root
@@ -4073,22 +3828,20 @@ describe('ReactDOMServerPartialHydration', () => {
3828 await act(() => {
3829 ReactDOMClient.hydrateRoot(container, <DirectTextChild text="bad" />, {
3830 onRecoverableError(error) {
4076 - Scheduler.log(error.message);
3831 + Scheduler.log(normalizeError(error.message));
3832 },
3833 });
3834 });
3835 }).toErrorDev(
3836 [
4082 - 'Text content did not match. Server: "good" Client: "bad"',
3837 'An error occurred during hydration. The server HTML was replaced with ' +
3838 'client content.',
3839 ],
3840 {withoutStack: 1},
3841 );
3842 assertLog([
4089 - 'Text content does not match server-rendered HTML.',
4090 - 'There was an error while hydrating. Because the error happened outside ' +
4091 - 'of a Suspense boundary, the entire root will switch to client rendering.',
3843 + "Hydration failed because the server rendered HTML didn't match the client.",
3844 + 'There was an error while hydrating.',
3845 ]);
3846 });
3847
@@ -4116,23 +3869,21 @@ describe('ReactDOMServerPartialHydration', () => {
3869 <TextChildWithSibling text="bad" />,
3870 {
3871 onRecoverableError(error) {
4119 - Scheduler.log(error.message);
3872 + Scheduler.log(normalizeError(error.message));
3873 },
3874 },
3875 );
3876 });
3877 }).toErrorDev(
3878 [
4126 - 'Text content did not match. Server: "good" Client: "bad"',
3879 'An error occurred during hydration. The server HTML was replaced with ' +
3880 'client content.',
3881 ],
3882 {withoutStack: 1},
3883 );
3884 assertLog([
4133 - 'Text content does not match server-rendered HTML.',
4134 - 'There was an error while hydrating. Because the error happened outside ' +
4135 - 'of a Suspense boundary, the entire root will switch to client rendering.',
3885 + "Hydration failed because the server rendered HTML didn't match the client.",
3886 + 'There was an error while hydrating.',
3887 ]);
3888 });
3889 });
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+18 -11
@@ -24,6 +24,15 @@ let hasErrored = false;
24 let fatalError = undefined;
25 let waitForAll;
26
27 +function normalizeError(msg) {
28 + // Take the first sentence to make it easier to assert on.
29 + const idx = msg.indexOf('.');
30 + if (idx > -1) {
31 + return msg.slice(0, idx + 1);
32 + }
33 + return msg;
34 +}
35 +
36 describe('ReactDOM HostSingleton', () => {
37 beforeEach(() => {
38 jest.resetModules();
@@ -454,7 +463,7 @@ describe('ReactDOM HostSingleton', () => {
463 {
464 onRecoverableError(error, errorInfo) {
465 hydrationErrors.push([
457 - error.message,
466 + normalizeError(error.message),
467 errorInfo.componentStack
468 ? errorInfo.componentStack.split('\n')[1].trim()
469 : null,
@@ -466,23 +475,16 @@ describe('ReactDOM HostSingleton', () => {
475 await waitForAll([]);
476 }).toErrorDev(
477 [
469 - `Warning: Expected server HTML to contain a matching <div> in <body>.
470 - in div (at **)
471 - in body (at **)
472 - in html (at **)`,
478 `Warning: An error occurred during hydration. The server HTML was replaced with client content.`,
479 ],
480 {withoutStack: 1},
481 );
482 expect(hydrationErrors).toEqual([
483 [
479 - 'Hydration failed because the initial UI does not match what was rendered on the server.',
484 + "Hydration failed because the server rendered HTML didn't match the client.",
485 'at div',
486 ],
482 - [
483 - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
484 - null,
485 - ],
487 + ['There was an error while hydrating.', null],
488 ]);
489 expect(persistentElements).toEqual([
490 document.documentElement,
@@ -546,7 +548,12 @@ describe('ReactDOM HostSingleton', () => {
548 },
549 );
550 expect(hydrationErrors).toEqual([]);
549 - await waitForAll([]);
551 + await expect(async () => {
552 + await waitForAll([]);
553 + }).toErrorDev(
554 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
555 + {withoutStack: true},
556 + );
557 expect(persistentElements).toEqual([
558 document.documentElement,
559 document.head,
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+29 -16
@@ -28,6 +28,15 @@ function getTestDocument(markup) {
28 return doc;
29 }
30
31 +function normalizeError(msg) {
32 + // Take the first sentence to make it easier to assert on.
33 + const idx = msg.indexOf('.');
34 + if (idx > -1) {
35 + return msg.slice(0, idx + 1);
36 + }
37 + return msg;
38 +}
39 +
40 describe('rendering React components at document', () => {
41 beforeEach(() => {
42 jest.resetModules();
@@ -191,21 +200,22 @@ describe('rendering React components at document', () => {
200 ReactDOM.flushSync(() => {
201 ReactDOMClient.hydrateRoot(container, <div>parsnip</div>, {
202 onRecoverableError: error => {
194 - Scheduler.log('Log recoverable error: ' + error.message);
203 + Scheduler.log(
204 + 'Log recoverable error: ' + normalizeError(error.message),
205 + );
206 },
207 });
208 });
209 }).toErrorDev(
210 [
211 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
201 - 'Expected server HTML to contain a matching <div> in the root.',
212 ],
213 {withoutStack: 1},
214 );
215
216 assertLog([
207 - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
208 - 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
217 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
218 + 'Log recoverable error: There was an error while hydrating.',
219 ]);
220
221 // This creates an unfortunate double text case.
@@ -226,7 +236,9 @@ describe('rendering React components at document', () => {
236 </div>,
237 {
238 onRecoverableError: error => {
229 - Scheduler.log('Log recoverable error: ' + error.message);
239 + Scheduler.log(
240 + 'Log recoverable error: ' + normalizeError(error.message),
241 + );
242 },
243 },
244 );
@@ -234,14 +246,13 @@ describe('rendering React components at document', () => {
246 }).toErrorDev(
247 [
248 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
237 - 'Expected server HTML to contain a matching <div> in <div>.',
249 ],
250 {withoutStack: 1},
251 );
252
253 assertLog([
243 - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
244 - 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
254 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
255 + 'Log recoverable error: There was an error while hydrating.',
256 ]);
257 expect(container.textContent).toBe('parsnip');
258 });
@@ -272,7 +283,9 @@ describe('rendering React components at document', () => {
283 <Component text="Hello world" />,
284 {
285 onRecoverableError: error => {
275 - Scheduler.log('Log recoverable error: ' + error.message);
286 + Scheduler.log(
287 + 'Log recoverable error: ' + normalizeError(error.message),
288 + );
289 },
290 },
291 );
@@ -280,7 +293,6 @@ describe('rendering React components at document', () => {
293 }).toErrorDev(
294 [
295 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
283 - 'Warning: Text content did not match.',
296 ],
297 {
298 withoutStack: 1,
@@ -288,8 +300,8 @@ describe('rendering React components at document', () => {
300 );
301
302 assertLog([
291 - 'Log recoverable error: Text content does not match server-rendered HTML.',
292 - 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
303 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
304 + 'Log recoverable error: There was an error while hydrating.',
305 ]);
306 expect(testDocument.body.innerHTML).toBe('Hello world');
307 });
@@ -318,7 +330,9 @@ describe('rendering React components at document', () => {
330 <Component text="Hello world" />,
331 {
332 onRecoverableError: error => {
321 - Scheduler.log('Log recoverable error: ' + error.message);
333 + Scheduler.log(
334 + 'Log recoverable error: ' + normalizeError(error.message),
335 + );
336 },
337 },
338 );
@@ -326,13 +340,12 @@ describe('rendering React components at document', () => {
340 }).toErrorDev(
341 [
342 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
329 - 'Expected server HTML to contain a matching text node for "Hello world" in <body>',
343 ],
344 {withoutStack: 1},
345 );
346 assertLog([
334 - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
335 - 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
347 + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
348 + 'Log recoverable error: There was an error while hydrating.',
349 ]);
350 expect(testDocument.body.innerHTML).toBe('Hello world');
351 });
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+4 -12
@@ -141,7 +141,6 @@ describe('ReactDOMServerHydration', () => {
141 }).toErrorDev(
142 [
143 'An error occurred during hydration. The server HTML was replaced with client content.',
144 - 'Text content did not match. Server: "x" Client: "y"',
144 ],
145 {withoutStack: 1},
146 );
@@ -226,7 +225,6 @@ describe('ReactDOMServerHydration', () => {
225 }).toErrorDev(
226 [
227 'An error occurred during hydration. The server HTML was replaced with client content.',
229 - 'Warning: Text content did not match. Server: "server" Client: "client"',
228 ],
229 {withoutStack: 1},
230 );
@@ -253,10 +251,8 @@ describe('ReactDOMServerHydration', () => {
251 );
252 });
253 }).toErrorDev(
256 - 'Warning: Prop `style` did not match. Server: ' +
257 - '{"text-decoration":"none","color":"black","height":"10px"}' +
258 - ' Client: ' +
259 - '{"textDecoration":"none","color":"white","height":"10px"}',
254 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
255 + {withoutStack: true},
256 );
257 });
258
@@ -303,10 +299,8 @@ describe('ReactDOMServerHydration', () => {
299 );
300 });
301 }).toErrorDev(
306 - 'Warning: Prop `style` did not match. Server: ' +
307 - '{"text-decoration":"none","color":"black","height":"10px"}' +
308 - ' Client: ' +
309 - '{"textDecoration":"none","color":"black","height":"10px"}', // note that this is no difference
302 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
303 + {withoutStack: true},
304 );
305 });
306
@@ -535,7 +529,6 @@ describe('ReactDOMServerHydration', () => {
529 }).toErrorDev(
530 [
531 'An error occurred during hydration. The server HTML was replaced with client content.',
538 - 'Warning: Text content did not match. Server: "server" Client: "client"',
532 ],
533 {withoutStack: 1},
534 );
@@ -561,7 +554,6 @@ describe('ReactDOMServerHydration', () => {
554 }).toErrorDev(
555 [
556 'An error occurred during hydration. The server HTML was replaced with client content.',
564 - 'Warning: Did not expect server HTML to contain a <p> in <div>.',
557 ],
558 {withoutStack: 1},
559 );
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+11 -2
@@ -53,8 +53,17 @@ module.exports = function (initModules) {
53 if (forceHydrate) {
54 await act(() => {
55 ReactDOMClient.hydrateRoot(domElement, reactElement, {
56 - onRecoverableError: () => {
57 - // TODO: assert on recoverable error count.
56 + onRecoverableError(e) {
57 + if (
58 + e.message.startsWith(
59 + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
60 + )
61 + ) {
62 + // We ignore this extra error because it shouldn't really need to be there if
63 + // a hydration mismatch is the cause of it.
64 + } else {
65 + console.error(e);
66 + }
67 },
68 });
69 });
packages/react-reconciler/src/ReactFiberCompleteWork.js
+3
@@ -144,6 +144,7 @@ import {
144 resetHydrationState,
145 getIsHydrating,
146 upgradeHydrationErrorsToRecoverable,
147 + emitPendingHydrationWarnings,
148 } from './ReactFiberHydrationContext';
149 import {
150 renderHasNotSuspendedYet,
@@ -893,6 +894,7 @@ function completeDehydratedSuspenseBoundary(
894 }
895 return false;
896 } else {
897 + emitPendingHydrationWarnings();
898 // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
899 // state since we're now exiting out of it. popHydrationState doesn't do that for us.
900 resetHydrationState();
@@ -1009,6 +1011,7 @@ function completeWork(
1011 // that weren't hydrated.
1012 const wasHydrated = popHydrationState(workInProgress);
1013 if (wasHydrated) {
1014 + emitPendingHydrationWarnings();
1015 // If we hydrated, then we'll need to schedule an update for
1016 // the commit side-effects on the root.
1017 markUpdate(workInProgress);
packages/react-reconciler/src/ReactFiberHydrationContext.js
+120 -234
@@ -19,11 +19,11 @@ import type {
19 import type {SuspenseState} from './ReactFiberSuspenseComponent';
20 import type {TreeContext} from './ReactFiberTreeContext';
21 import type {CapturedValue} from './ReactCapturedValue';
22 +import type {HydrationDiffNode} from './ReactFiberHydrationDiffs';
23
24 import {
25 HostComponent,
26 HostSingleton,
26 - HostText,
27 HostRoot,
28 SuspenseComponent,
29 } from './ReactWorkTags';
@@ -61,6 +61,7 @@ import {
61 } from './ReactFiberTreeContext';
62 import {queueRecoverableErrors} from './ReactFiberWorkLoop';
63 import {getRootHostContainer, getHostContext} from './ReactFiberHostContext';
64 +import {describeDiff} from './ReactFiberHydrationDiffs';
65
66 // The deepest Fiber on the stack involved in a hydration context.
67 // This may have been an insertion or a hydration.
@@ -72,11 +73,50 @@ let isHydrating: boolean = false;
73 // due to earlier mismatches or a suspended fiber.
74 let didSuspendOrErrorDEV: boolean = false;
75
76 +// Hydration differences found that haven't yet been logged.
77 +let hydrationDiffRootDEV: null | HydrationDiffNode = null;
78 +
79 // Hydration errors that were thrown inside this boundary
80 let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
81
82 let rootOrSingletonContext = false;
83
84 +// Builds a common ancestor tree from the root down for collecting diffs.
85 +function buildHydrationDiffNode(fiber: Fiber): HydrationDiffNode {
86 + if (fiber.return === null) {
87 + // We're at the root.
88 + if (hydrationDiffRootDEV === null) {
89 + hydrationDiffRootDEV = {
90 + fiber: fiber,
91 + children: [],
92 + serverProps: undefined,
93 + serverTail: [],
94 + };
95 + } else if (hydrationDiffRootDEV.fiber !== fiber) {
96 + throw new Error(
97 + 'Saw multiple hydration diff roots in a pass. This is a bug in React.',
98 + );
99 + }
100 + return hydrationDiffRootDEV;
101 + }
102 + const siblings = buildHydrationDiffNode(fiber.return).children;
103 + // The same node may already exist in the parent. Since we currently always render depth first
104 + // and rerender if we suspend or terminate early, if a shared ancestor was added we should still
105 + // be inside of that shared ancestor which means it was the last one to be added. If this changes
106 + // we may have to scan the whole set.
107 + if (siblings.length > 0 && siblings[siblings.length - 1].fiber === fiber) {
108 + return siblings[siblings.length - 1];
109 + }
110 + const newNode: HydrationDiffNode = {
111 + fiber: fiber,
112 + children: [],
113 + serverProps: undefined,
114 + serverTail: [],
115 + };
116 + siblings.push(newNode);
117 + return newNode;
118 +}
119 +
120 function warnIfHydrating() {
121 if (__DEV__) {
122 if (isHydrating) {
@@ -105,6 +145,7 @@ function enterHydrationState(fiber: Fiber): boolean {
145 isHydrating = true;
146 hydrationErrors = null;
147 didSuspendOrErrorDEV = false;
148 + hydrationDiffRootDEV = null;
149 rootOrSingletonContext = true;
150 return true;
151 }
@@ -123,6 +164,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
164 isHydrating = true;
165 hydrationErrors = null;
166 didSuspendOrErrorDEV = false;
167 + hydrationDiffRootDEV = null;
168 rootOrSingletonContext = false;
169 if (treeContext !== null) {
170 restoreSuspendedTreeContext(fiber, treeContext);
@@ -130,58 +172,6 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
172 return true;
173 }
174
133 -function warnForDeletedHydratableInstance(
134 - parentType: string,
135 - child: HydratableInstance,
136 -) {
137 - if (__DEV__) {
138 - const description = describeHydratableInstanceForDevWarnings(child);
139 - if (typeof description === 'string') {
140 - console.error(
141 - 'Did not expect server HTML to contain the text node "%s" in <%s>.',
142 - description,
143 - parentType,
144 - );
145 - } else {
146 - console.error(
147 - 'Did not expect server HTML to contain a <%s> in <%s>.',
148 - description.type,
149 - parentType,
150 - );
151 - }
152 - }
153 -}
154 -
155 -function warnForInsertedHydratedElement(parentType: string, tag: string) {
156 - if (__DEV__) {
157 - console.error(
158 - 'Expected server HTML to contain a matching <%s> in <%s>.',
159 - tag,
160 - parentType,
161 - );
162 - }
163 -}
164 -
165 -function warnForInsertedHydratedText(parentType: string, text: string) {
166 - if (__DEV__) {
167 - console.error(
168 - 'Expected server HTML to contain a matching text node for "%s" in <%s>.',
169 - text,
170 - parentType,
171 - );
172 - }
173 -}
174 -
175 -function warnForInsertedHydratedSuspense(parentType: string) {
176 - if (__DEV__) {
177 - console.error(
178 - 'Expected server HTML to contain a matching <%s> in <%s>.',
179 - 'Suspense',
180 - parentType,
181 - );
182 - }
183 -}
184 -
175 export function errorHydratingContainer(parentContainer: Container): void {
176 if (__DEV__) {
177 // TODO: This gets logged by onRecoverableError, too, so we should be
@@ -192,48 +182,7 @@ export function errorHydratingContainer(parentContainer: Container): void {
182 }
183 }
184
195 -function warnUnhydratedInstance(
196 - returnFiber: Fiber,
197 - instance: HydratableInstance,
198 -) {
199 - if (__DEV__) {
200 - if (didWarnInvalidHydration) {
201 - return;
202 - }
203 - didWarnInvalidHydration = true;
204 -
205 - switch (returnFiber.tag) {
206 - case HostRoot: {
207 - const description = describeHydratableInstanceForDevWarnings(instance);
208 - if (typeof description === 'string') {
209 - console.error(
210 - 'Did not expect server HTML to contain the text node "%s" in the root.',
211 - description,
212 - );
213 - } else {
214 - console.error(
215 - 'Did not expect server HTML to contain a <%s> in the root.',
216 - description.type,
217 - );
218 - }
219 - break;
220 - }
221 - case HostSingleton:
222 - case HostComponent: {
223 - warnForDeletedHydratableInstance(returnFiber.type, instance);
224 - break;
225 - }
226 - case SuspenseComponent: {
227 - const suspenseState: SuspenseState = returnFiber.memoizedState;
228 - if (suspenseState.dehydrated !== null)
229 - warnForDeletedHydratableInstance('Suspense', instance);
230 - break;
231 - }
232 - }
233 - }
234 -}
235 -
236 -function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
185 +function warnNonHydratedInstance(fiber: Fiber) {
186 if (__DEV__) {
187 if (didSuspendOrErrorDEV) {
188 // Inside a boundary that already suspended. We're currently rendering the
@@ -242,84 +191,10 @@ function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
191 return;
192 }
193
245 - if (didWarnInvalidHydration) {
246 - return;
247 - }
248 - didWarnInvalidHydration = true;
249 -
250 - switch (returnFiber.tag) {
251 - case HostRoot: {
252 - // const parentContainer = returnFiber.stateNode.containerInfo;
253 - switch (fiber.tag) {
254 - case HostSingleton:
255 - case HostComponent:
256 - console.error(
257 - 'Expected server HTML to contain a matching <%s> in the root.',
258 - fiber.type,
259 - );
260 - break;
261 - case HostText:
262 - const text = fiber.pendingProps;
263 - console.error(
264 - 'Expected server HTML to contain a matching text node for "%s" in the root.',
265 - text,
266 - );
267 - break;
268 - case SuspenseComponent:
269 - console.error(
270 - 'Expected server HTML to contain a matching <%s> in the root.',
271 - 'Suspense',
272 - );
273 - break;
274 - }
275 - break;
276 - }
277 - case HostSingleton:
278 - case HostComponent: {
279 - const parentType = returnFiber.type;
280 - // const parentProps = returnFiber.memoizedProps;
281 - // const parentInstance = returnFiber.stateNode;
282 - switch (fiber.tag) {
283 - case HostSingleton:
284 - case HostComponent: {
285 - const type = fiber.type;
286 - warnForInsertedHydratedElement(parentType, type);
287 - break;
288 - }
289 - case HostText: {
290 - const text = fiber.pendingProps;
291 - warnForInsertedHydratedText(parentType, text);
292 - break;
293 - }
294 - case SuspenseComponent: {
295 - warnForInsertedHydratedSuspense(parentType);
296 - break;
297 - }
298 - }
299 - break;
300 - }
301 - case SuspenseComponent: {
302 - // const suspenseState: SuspenseState = returnFiber.memoizedState;
303 - // const parentInstance = suspenseState.dehydrated;
304 - switch (fiber.tag) {
305 - case HostSingleton:
306 - case HostComponent:
307 - const type = fiber.type;
308 - warnForInsertedHydratedElement('Suspense', type);
309 - break;
310 - case HostText:
311 - const text = fiber.pendingProps;
312 - warnForInsertedHydratedText('Suspense', text);
313 - break;
314 - case SuspenseComponent:
315 - warnForInsertedHydratedSuspense('Suspense');
316 - break;
317 - }
318 - break;
319 - }
320 - default:
321 - return;
322 - }
194 + // Add this fiber to the diff tree.
195 + const diffNode = buildHydrationDiffNode(fiber);
196 + // We use null as a signal that there was no node to match.
197 + diffNode.serverProps = null;
198 }
199 }
200
@@ -390,9 +265,29 @@ function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
265 }
266
267 function throwOnHydrationMismatch(fiber: Fiber) {
268 + let diff = '';
269 + if (__DEV__) {
270 + // Consume the diff root for this mismatch.
271 + // Any other errors will get their own diffs.
272 + const diffRoot = hydrationDiffRootDEV;
273 + if (diffRoot !== null) {
274 + hydrationDiffRootDEV = null;
275 + diff = describeDiff(diffRoot);
276 + }
277 + }
278 throw new Error(
394 - 'Hydration failed because the initial UI does not match what was ' +
395 - 'rendered on the server.',
279 + "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n" +
280 + '\n' +
281 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
282 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
283 + "- Date formatting in a user's locale which doesn't match the server.\n" +
284 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
285 + '- Invalid HTML tag nesting.\n' +
286 + '\n' +
287 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
288 + '\n' +
289 + 'https://react.dev/link/hydration-mismatch' +
290 + diff,
291 );
292 }
293
@@ -432,7 +327,7 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
327 const nextInstance = nextHydratableInstance;
328 if (!nextInstance || !tryHydrateInstance(fiber, nextInstance)) {
329 if (shouldKeepWarning) {
435 - warnNonHydratedInstance((hydrationParentFiber: any), fiber);
330 + warnNonHydratedInstance(fiber);
331 }
332 throwOnHydrationMismatch(fiber);
333 }
@@ -452,7 +347,7 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
347 const nextInstance = nextHydratableInstance;
348 if (!nextInstance || !tryHydrateText(fiber, nextInstance)) {
349 if (shouldKeepWarning) {
455 - warnNonHydratedInstance((hydrationParentFiber: any), fiber);
350 + warnNonHydratedInstance(fiber);
351 }
352 throwOnHydrationMismatch(fiber);
353 }
@@ -464,7 +359,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
359 }
360 const nextInstance = nextHydratableInstance;
361 if (!nextInstance || !tryHydrateSuspense(fiber, nextInstance)) {
467 - warnNonHydratedInstance((hydrationParentFiber: any), fiber);
362 + warnNonHydratedInstance(fiber);
363 throwOnHydrationMismatch(fiber);
364 }
365 }
@@ -497,9 +392,6 @@ export function tryToClaimNextHydratableFormMarkerInstance(
392 return false;
393 }
394
500 -// Temp
501 -let didWarnInvalidHydration = false;
502 -
395 function prepareToHydrateHostInstance(
396 fiber: Fiber,
397 hostContext: HostContext,
@@ -522,39 +414,8 @@ function prepareToHydrateHostInstance(
414 hostContext,
415 );
416 if (differences !== null) {
525 - if (differences.children != null && !didWarnInvalidHydration) {
526 - didWarnInvalidHydration = true;
527 - const serverValue = differences.children;
528 - const clientValue = fiber.memoizedProps.children;
529 - console.error(
530 - 'Text content did not match. Server: "%s" Client: "%s"',
531 - serverValue,
532 - clientValue,
533 - );
534 - }
535 - for (const propName in differences) {
536 - if (!differences.hasOwnProperty(propName)) {
537 - continue;
538 - }
539 - if (didWarnInvalidHydration) {
540 - break;
541 - }
542 - didWarnInvalidHydration = true;
543 - const serverValue = differences[propName];
544 - const clientValue = fiber.memoizedProps[propName];
545 - if (propName === 'children') {
546 - // Already handled above
547 - } else if (clientValue != null) {
548 - console.error(
549 - 'Prop `%s` did not match. Server: %s Client: %s',
550 - propName,
551 - JSON.stringify(serverValue),
552 - JSON.stringify(clientValue),
553 - );
554 - } else {
555 - console.error('Extra attribute from the server: %s', propName);
556 - }
557 - }
417 + const diffNode = buildHydrationDiffNode(fiber);
418 + diffNode.serverProps = differences;
419 }
420 }
421 }
@@ -567,7 +428,7 @@ function prepareToHydrateHostInstance(
428 fiber,
429 );
430 if (!didHydrate) {
570 - throw new Error('Text content does not match server-rendered HTML.');
431 + throwOnHydrationMismatch(fiber);
432 }
433 }
434
@@ -596,13 +457,9 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
457 textContent,
458 parentProps,
459 );
599 - if (difference !== null && !didWarnInvalidHydration) {
600 - didWarnInvalidHydration = true;
601 - console.error(
602 - 'Text content did not match. Server: "%s" Client: "%s"',
603 - difference,
604 - textContent,
605 - );
460 + if (difference !== null) {
461 + const diffNode = buildHydrationDiffNode(fiber);
462 + diffNode.serverProps = difference;
463 }
464 }
465 }
@@ -618,13 +475,9 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
475 textContent,
476 parentProps,
477 );
621 - if (difference !== null && !didWarnInvalidHydration) {
622 - didWarnInvalidHydration = true;
623 - console.error(
624 - 'Text content did not match. Server: "%s" Client: "%s"',
625 - difference,
626 - textContent,
627 - );
478 + if (difference !== null) {
479 + const diffNode = buildHydrationDiffNode(fiber);
480 + diffNode.serverProps = difference;
481 }
482 }
483 }
@@ -641,7 +494,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
494 parentProps,
495 );
496 if (!didHydrate) {
644 - throw new Error('Text content does not match server-rendered HTML.');
497 + throwOnHydrationMismatch(fiber);
498 }
499 }
500
@@ -774,10 +627,15 @@ function popHydrationState(fiber: Fiber): boolean {
627 }
628
629 function warnIfUnhydratedTailNodes(fiber: Fiber) {
777 - let nextInstance = nextHydratableInstance;
778 - while (nextInstance) {
779 - warnUnhydratedInstance(fiber, nextInstance);
780 - nextInstance = getNextHydratableSibling(nextInstance);
630 + if (__DEV__) {
631 + let nextInstance = nextHydratableInstance;
632 + while (nextInstance) {
633 + const diffNode = buildHydrationDiffNode(fiber);
634 + const description =
635 + describeHydratableInstanceForDevWarnings(nextInstance);
636 + diffNode.serverTail.push(description);
637 + nextInstance = getNextHydratableSibling(nextInstance);
638 + }
639 }
640 }
641
@@ -814,6 +672,34 @@ export function queueHydrationError(error: CapturedValue<mixed>): void {
672 }
673 }
674
675 +export function emitPendingHydrationWarnings() {
676 + if (__DEV__) {
677 + // If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
678 + // hydrated, however, we might still have DEV-only mismatches that we log now.
679 + const diffRoot = hydrationDiffRootDEV;
680 + if (diffRoot !== null) {
681 + hydrationDiffRootDEV = null;
682 + const diff = describeDiff(diffRoot);
683 + console.error(
684 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
685 + 'This can happen if a SSR-ed Client Component used:\n' +
686 + '\n' +
687 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
688 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
689 + "- Date formatting in a user's locale which doesn't match the server.\n" +
690 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
691 + '- Invalid HTML tag nesting.\n' +
692 + '\n' +
693 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
694 + '\n' +
695 + '%s%s',
696 + 'https://react.dev/link/hydration-mismatch',
697 + diff,
698 + );
699 + }
700 + }
701 +}
702 +
703 export {
704 warnIfHydrating,
705 enterHydrationState,
packages/react-reconciler/src/ReactFiberHydrationDiffs.js new
+24
@@ -0,0 +1,24 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Fiber} from './ReactInternalTypes';
11 +
12 +export type HydrationDiffNode = {
13 + fiber: Fiber,
14 + children: Array<HydrationDiffNode>,
15 + serverProps: void | null | $ReadOnly<{[propName: string]: mixed}> | string, // null means no matching server node
16 + serverTail: Array<
17 + | $ReadOnly<{type: string, props: $ReadOnly<{[propName: string]: mixed}>}>
18 + | string,
19 + >,
20 +};
21 +
22 +export function describeDiff(rootNode: HydrationDiffNode): string {
23 + return '\n';
24 +}
scripts/error-codes/codes.json
+3 -2
@@ -403,7 +403,7 @@
403 "415": "Error parsing the data. It's probably an error code or network corruption.",
404 "416": "This environment don't support binary chunks.",
405 "417": "React currently only supports piping to one writable stream.",
406 - "418": "Hydration failed because the initial UI does not match what was rendered on the server.",
406 + "418": "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\nhttps://react.dev/link/hydration-mismatch%s",
407 "419": "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.",
408 "420": "ServerContext: %s already defined",
409 "421": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.",
@@ -502,5 +502,6 @@
502 "514": "Cannot access %s on the server. You cannot dot into a temporary client reference from a server component. You can only pass the value through to the client.",
503 "515": "Cannot assign to a temporary client reference from a server module.",
504 "516": "Attempted to call a temporary Client Reference from the server but it is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
505 - "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s"
505 + "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s",
506 + "518": "Saw multiple hydration diff roots in a pass. This is a bug in React."
507 }