Skip to content

Commit 1dece52

Browse files
authored
Add back warning with component stack on Hydration mismatch (#23241)
* add back warning * wrapper errorMock in __DEV__ flag * lint
1 parent cd4eb11 commit 1dece52

File tree

4 files changed

+202
-125
lines changed

4 files changed

+202
-125
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -1740,8 +1740,13 @@ describe('ReactDOMFizzServer', () => {
17401740
'The server HTML was replaced with client content',
17411741
]);
17421742
}).toErrorDev(
1743-
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1744-
{withoutStack: true},
1743+
[
1744+
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
1745+
'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
1746+
' in div (at **)\n' +
1747+
' in App (at **)',
1748+
],
1749+
{withoutStack: 1},
17451750
);
17461751
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
17471752
} else {
@@ -1833,8 +1838,13 @@ describe('ReactDOMFizzServer', () => {
18331838
'The server HTML was replaced with client content',
18341839
]);
18351840
}).toErrorDev(
1836-
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1837-
{withoutStack: true},
1841+
[
1842+
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1843+
'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
1844+
' in div (at **)\n' +
1845+
' in App (at **)',
1846+
],
1847+
{withoutStack: 1},
18381848
);
18391849
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
18401850
} else {

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

+152-99
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ let SuspenseList;
1919
let act;
2020
let IdleEventPriority;
2121

22+
function normalizeCodeLocInfo(strOrErr) {
23+
if (strOrErr && strOrErr.replace) {
24+
return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
25+
return '\n in ' + name + ' (at **)';
26+
});
27+
}
28+
return strOrErr;
29+
}
30+
2231
function dispatchMouseEvent(to, from) {
2332
if (!to) {
2433
to = null;
@@ -240,6 +249,12 @@ describe('ReactDOMServerPartialHydration', () => {
240249

241250
// @gate enableClientRenderFallbackOnHydrationMismatch
242251
it('falls back to client rendering boundary on mismatch', async () => {
252+
// We can't use the toErrorDev helper here because this is async.
253+
const originalConsoleError = console.error;
254+
const mockError = jest.fn();
255+
console.error = (...args) => {
256+
mockError(...args.map(normalizeCodeLocInfo));
257+
};
243258
let client = false;
244259
let suspend = false;
245260
let resolve;
@@ -276,70 +291,86 @@ describe('ReactDOMServerPartialHydration', () => {
276291
</Suspense>
277292
);
278293
}
279-
const finalHTML = ReactDOMServer.renderToString(<App />);
280-
const container = document.createElement('div');
281-
container.innerHTML = finalHTML;
282-
expect(Scheduler).toHaveYielded([
283-
'Hello',
284-
'Component',
285-
'Component',
286-
'Component',
287-
'Component',
288-
]);
294+
try {
295+
const finalHTML = ReactDOMServer.renderToString(<App />);
296+
const container = document.createElement('div');
297+
container.innerHTML = finalHTML;
298+
expect(Scheduler).toHaveYielded([
299+
'Hello',
300+
'Component',
301+
'Component',
302+
'Component',
303+
'Component',
304+
]);
289305

290-
expect(container.innerHTML).toBe(
291-
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
292-
);
306+
expect(container.innerHTML).toBe(
307+
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
308+
);
293309

294-
suspend = true;
295-
client = true;
310+
suspend = true;
311+
client = true;
296312

297-
ReactDOM.hydrateRoot(container, <App />, {
298-
onRecoverableError(error) {
299-
Scheduler.unstable_yieldValue(error.message);
300-
},
301-
});
302-
expect(Scheduler).toFlushAndYield([
303-
'Suspend',
304-
'Component',
305-
'Component',
306-
'Component',
307-
'Component',
308-
]);
309-
jest.runAllTimers();
313+
ReactDOM.hydrateRoot(container, <App />, {
314+
onRecoverableError(error) {
315+
Scheduler.unstable_yieldValue(error.message);
316+
},
317+
});
318+
expect(Scheduler).toFlushAndYield([
319+
'Suspend',
320+
'Component',
321+
'Component',
322+
'Component',
323+
'Component',
324+
]);
325+
jest.runAllTimers();
310326

311-
// Unchanged
312-
expect(container.innerHTML).toBe(
313-
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
314-
);
327+
// Unchanged
328+
expect(container.innerHTML).toBe(
329+
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
330+
);
315331

316-
suspend = false;
317-
resolve();
318-
await promise;
332+
suspend = false;
333+
resolve();
334+
await promise;
335+
expect(Scheduler).toFlushAndYield([
336+
// first pass, mismatches at end
337+
'Hello',
338+
'Component',
339+
'Component',
340+
'Component',
341+
'Component',
342+
343+
// second pass as client render
344+
'Hello',
345+
'Component',
346+
'Component',
347+
'Component',
348+
'Component',
349+
350+
// Hydration mismatch is logged
351+
'An error occurred during hydration. The server HTML was replaced with client content',
352+
]);
319353

320-
expect(Scheduler).toFlushAndYield([
321-
// first pass, mismatches at end
322-
'Hello',
323-
'Component',
324-
'Component',
325-
'Component',
326-
'Component',
327-
328-
// second pass as client render
329-
'Hello',
330-
'Component',
331-
'Component',
332-
'Component',
333-
'Component',
334-
335-
// Hydration mismatch is logged
336-
'An error occurred during hydration. The server HTML was replaced with client content',
337-
]);
354+
// Client rendered - suspense comment nodes removed
355+
expect(container.innerHTML).toBe(
356+
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
357+
);
338358

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-
);
359+
if (__DEV__) {
360+
expect(mockError.mock.calls[0]).toEqual([
361+
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
362+
'div',
363+
'div',
364+
'\n' +
365+
' in div (at **)\n' +
366+
' in Component (at **)\n' +
367+
' in Suspense (at **)\n' +
368+
' in App (at **)',
369+
]);
370+
}
371+
} finally {
372+
console.error = originalConsoleError;
373+
}
343374
});
344375

345376
it('calls the hydration callbacks after hydration or deletion', async () => {
@@ -493,21 +524,14 @@ describe('ReactDOMServerPartialHydration', () => {
493524
});
494525

495526
it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
496-
spyOnDev(console, 'error');
497-
const ref = React.createRef();
498-
function App({hasB}) {
499-
return (
500-
<div>
501-
<Suspense fallback="Loading...">
502-
<Suspender />
503-
<span ref={ref}>A</span>
504-
{hasB ? <span>B</span> : null}
505-
</Suspense>
506-
<div>Sibling</div>
507-
</div>
508-
);
509-
}
527+
// We can't use the toErrorDev helper here because this is async.
528+
const originalConsoleError = console.error;
529+
const mockError = jest.fn();
530+
console.error = (...args) => {
531+
mockError(...args.map(normalizeCodeLocInfo));
532+
};
510533

534+
const ref = React.createRef();
511535
let shouldSuspend = false;
512536
let resolve;
513537
const promise = new Promise(res => {
@@ -522,37 +546,61 @@ describe('ReactDOMServerPartialHydration', () => {
522546
}
523547
return <></>;
524548
}
549+
function App({hasB}) {
550+
return (
551+
<div>
552+
<Suspense fallback="Loading...">
553+
<Suspender />
554+
<span ref={ref}>A</span>
555+
{hasB ? <span>B</span> : null}
556+
</Suspense>
557+
<div>Sibling</div>
558+
</div>
559+
);
560+
}
561+
try {
562+
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
525563

526-
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
527-
528-
const container = document.createElement('div');
529-
container.innerHTML = finalHTML;
564+
const container = document.createElement('div');
565+
container.innerHTML = finalHTML;
530566

531-
const span = container.getElementsByTagName('span')[0];
567+
const span = container.getElementsByTagName('span')[0];
532568

533-
expect(container.innerHTML).toContain('<span>A</span>');
534-
expect(container.innerHTML).toContain('<span>B</span>');
535-
expect(ref.current).toBe(null);
569+
expect(container.innerHTML).toContain('<span>A</span>');
570+
expect(container.innerHTML).toContain('<span>B</span>');
571+
expect(ref.current).toBe(null);
536572

537-
shouldSuspend = true;
538-
act(() => {
539-
ReactDOM.hydrateRoot(container, <App hasB={false} />);
540-
});
573+
shouldSuspend = true;
574+
act(() => {
575+
ReactDOM.hydrateRoot(container, <App hasB={false} />);
576+
});
541577

542-
// await expect(async () => {
543-
resolve();
544-
await promise;
545-
Scheduler.unstable_flushAll();
546-
await null;
547-
jest.runAllTimers();
548-
// }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
578+
resolve();
579+
await promise;
580+
Scheduler.unstable_flushAll();
581+
await null;
582+
jest.runAllTimers();
549583

550-
expect(container.innerHTML).toContain('<span>A</span>');
551-
expect(container.innerHTML).not.toContain('<span>B</span>');
552-
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
553-
expect(ref.current).not.toBe(span);
554-
} else {
555-
expect(ref.current).toBe(span);
584+
expect(container.innerHTML).toContain('<span>A</span>');
585+
expect(container.innerHTML).not.toContain('<span>B</span>');
586+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
587+
expect(ref.current).not.toBe(span);
588+
} else {
589+
expect(ref.current).toBe(span);
590+
}
591+
if (__DEV__) {
592+
expect(mockError).toHaveBeenCalledWith(
593+
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
594+
'span',
595+
'div',
596+
'\n' +
597+
' in Suspense (at **)\n' +
598+
' in div (at **)\n' +
599+
' in App (at **)',
600+
);
601+
}
602+
} finally {
603+
console.error = originalConsoleError;
556604
}
557605
});
558606

@@ -3179,9 +3227,14 @@ describe('ReactDOMServerPartialHydration', () => {
31793227
});
31803228
});
31813229
}).toErrorDev(
3182-
'Warning: An error occurred during hydration. ' +
3183-
'The server HTML was replaced with client content in <div>.',
3184-
{withoutStack: true},
3230+
[
3231+
'Warning: An error occurred during hydration. ' +
3232+
'The server HTML was replaced with client content in <div>.',
3233+
'Warning: Expected server HTML to contain a matching <span> in <div>.\n' +
3234+
' in span (at **)\n' +
3235+
' in App (at **)',
3236+
],
3237+
{withoutStack: 1},
31853238
);
31863239
expect(Scheduler).toHaveYielded([
31873240
'Log recoverable error: An error occurred during hydration. The server ' +

0 commit comments

Comments
 (0)