diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index f64cdd8bb8e3..c7f52b1c6889 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -201,25 +201,24 @@ describe('ReactDOMFizzForm', () => { await act(async () => { ReactDOMClient.hydrateRoot(container, ); }); - assertConsoleErrorDev( - [ - "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:\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\n' + - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + - 'https://react.dev/link/hydration-mismatch\n\n' + - ' \n' + - ' \n', - ], - {withoutStack: true}, - ); + assertConsoleErrorDev([ + "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:\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\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + + 'https://react.dev/link/hydration-mismatch\n\n' + + ' \n' + + ' \n' + + '\n in form (at **)' + + '\n in App (at **)', + ]); }); it('should ideally warn when passing a string during SSR and function during hydration', async () => { @@ -392,40 +391,39 @@ describe('ReactDOMFizzForm', () => { await act(async () => { root = ReactDOMClient.hydrateRoot(container, ); }); - assertConsoleErrorDev( - [ - "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:\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\n' + - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + - 'https://react.dev/link/hydration-mismatch\n\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n', - ], - {withoutStack: true}, - ); + assertConsoleErrorDev([ + "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:\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\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + + 'https://react.dev/link/hydration-mismatch\n\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '\n in input (at **)' + + '\n in App (at **)', + ]); await act(async () => { root.render(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 5b44d9e02e85..754258252856 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -10233,7 +10233,7 @@ describe('ReactDOMFizzServer', () => { '\n+ client' + '\n- server' + '\n' + - '\n in Suspense (at **)' + + '\n in meta (at **)' + '\n in ClientApp (at **)', ]); } diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index 2efcfd01d65f..c445f458e5b1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -23,6 +23,7 @@ function errorHandler() { describe('ReactDOMServerHydration', () => { let container; + let ownerStacks; beforeEach(() => { jest.resetModules(); @@ -32,7 +33,15 @@ describe('ReactDOMServerHydration', () => { act = React.act; window.addEventListener('error', errorHandler); - console.error = jest.fn(); + ownerStacks = []; + console.error = jest.fn(() => { + const ownerStack = React.captureOwnerStack(); + if (typeof ownerStack === 'string') { + ownerStacks.push(ownerStack === '' ? ' ' : ownerStack); + } else { + ownerStacks.push(' ' + String(ownerStack)); + } + }); container = document.createElement('div'); document.body.appendChild(container); }); @@ -44,15 +53,25 @@ describe('ReactDOMServerHydration', () => { }); function normalizeCodeLocInfo(str) { - return ( - typeof str === 'string' && - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { - return '\n in ' + name + ' (at **)'; - }) - ); + return typeof str === 'string' + ? str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return '\n in ' + name + ' (at **)'; + }) + : str; } - function formatMessage(args) { + function formatMessage(args, index) { + const ownerStack = ownerStacks[index]; + + if (ownerStack === undefined) { + throw new Error( + 'Expected an owner stack for message ' + + index + + ':\n' + + util.format(...args), + ); + } + const [format, ...rest] = args; if (format instanceof Error) { if (format.cause instanceof Error) { @@ -61,13 +80,23 @@ describe('ReactDOMServerHydration', () => { format.message + ']\n Cause [' + format.cause.message + - ']' + ']\n Owner Stack:' + + normalizeCodeLocInfo(ownerStack) ); } - return 'Caught [' + format.message + ']'; + return ( + 'Caught [' + + format.message + + ']\n Owner Stack:' + + normalizeCodeLocInfo(ownerStack) + ); } rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]); - return util.format(format, ...rest); + return ( + util.format(format, ...rest) + + '\n Owner Stack:' + + normalizeCodeLocInfo(ownerStack) + ); } function formatConsoleErrors() { @@ -115,7 +144,10 @@ describe('ReactDOMServerHydration', () => {
+ client - server - ]", + ] + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); } else { @@ -138,7 +170,10 @@ describe('ReactDOMServerHydration', () => {
+ client - server - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); } @@ -177,7 +212,10 @@ describe('ReactDOMServerHydration', () => {
+ This markup contains an nbsp entity:   client text - This markup contains an nbsp entity:   server text - ]", + ] + Owner Stack: + in div (at **) + in Mismatch (at **)", ] `); } else { @@ -199,7 +237,10 @@ describe('ReactDOMServerHydration', () => {
+ This markup contains an nbsp entity:   client text - This markup contains an nbsp entity:   server text - ", + + Owner Stack: + in div (at **) + in Mismatch (at **)", ] `); } @@ -245,7 +286,10 @@ describe('ReactDOMServerHydration', () => { - __html: "server" }} > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -286,7 +330,10 @@ describe('ReactDOMServerHydration', () => { + dir="ltr" - dir="rtl" > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -327,7 +374,10 @@ describe('ReactDOMServerHydration', () => { + dir="ltr" - dir={null} > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -368,7 +418,10 @@ describe('ReactDOMServerHydration', () => { + dir={null} - dir="rtl" > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -409,7 +462,10 @@ describe('ReactDOMServerHydration', () => { + dir={null} - dir="rtl" > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -449,7 +505,78 @@ describe('ReactDOMServerHydration', () => { + style={{opacity:1}} - style={{opacity:"0"}} > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", + ] + `); + }); + + // @gate __DEV__ + it('picks the DFS-first Fiber as the error Owner', () => { + function LeftMismatch({isClient}) { + return
; + } + + function LeftIndirection({isClient}) { + return ; + } + + function MiddleMismatch({isClient}) { + return ; + } + + function RightMisMatch({isClient}) { + return

; + } + + function App({isClient}) { + return ( + <> + + + + + ); + } + expect(testMismatch(App)).toMatchInlineSnapshot(` + [ + "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: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + + + + +

+ + + +

+ + Owner Stack: + in div (at **) + in LeftMismatch (at **) + in LeftIndirection (at **) + in App (at **)", ] `); }); @@ -483,7 +610,10 @@ describe('ReactDOMServerHydration', () => {

+
- ]", + ] + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -518,7 +648,10 @@ describe('ReactDOMServerHydration', () => { +
-
... - ]", + ] + Owner Stack: + in header (at **) + in Mismatch (at **)", ] `); }); @@ -554,7 +687,10 @@ describe('ReactDOMServerHydration', () => { +
-