From 6b1ae49571b97b15177bda7c1e39a87b42331da9 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 5 Mar 2025 12:24:45 -0500 Subject: [PATCH 1/5] [ez] Remove unused netlify.toml (#32530) I don't think this is in use anymore --- netlify.toml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 netlify.toml diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 9920f41194..0000000000 --- a/netlify.toml +++ /dev/null @@ -1,9 +0,0 @@ -[build] - base = "" - publish = "fixtures/dom/build" - command = "yarn build --type=UMD_DEV && cd fixtures/dom/ && yarn && yarn build" - -[[redirects]] - from = "/*" - to = "/index.html" - status = 200 From aac177c48439ab294f72e8b5a85059daa3f8a5ee Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 5 Mar 2025 18:45:16 +0000 Subject: [PATCH 2/5] Support beforetoggle/toggle events for dialog (#32479) --- .../src/client/ReactDOMComponent.js | 2 ++ .../ReactDOMEventPropagation-test.js | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index efdfc66ff2..48b4d9472f 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -1280,6 +1280,8 @@ export function setInitialProperties( return; } case 'dialog': { + listenToNonDelegatedEvent('beforetoggle', domElement); + listenToNonDelegatedEvent('toggle', domElement); listenToNonDelegatedEvent('cancel', domElement); listenToNonDelegatedEvent('close', domElement); break; diff --git a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js index 1598329340..ebd3f9a540 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js @@ -1302,6 +1302,38 @@ describe('ReactDOMEventListener', () => { }); }); + it('onBeforeToggle Dialog API', async () => { + await testEmulatedBubblingEvent({ + type: 'dialog', + reactEvent: 'onBeforeToggle', + reactEventType: 'beforetoggle', + nativeEvent: 'beforetoggle', + dispatch(node) { + const e = new Event('beforetoggle', { + bubbles: false, + cancelable: true, + }); + node.dispatchEvent(e); + }, + }); + }); + + it('onToggle Dialog API', async () => { + await testEmulatedBubblingEvent({ + type: 'dialog', + reactEvent: 'onToggle', + reactEventType: 'toggle', + nativeEvent: 'toggle', + dispatch(node) { + const e = new Event('toggle', { + bubbles: false, + cancelable: true, + }); + node.dispatchEvent(e); + }, + }); + }); + it('onVolumeChange', async () => { await testEmulatedBubblingEvent({ type: 'video', From e81fcfe3f201a8f626e892fb52ccbd0edba627cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 5 Mar 2025 22:16:56 -0500 Subject: [PATCH 3/5] [Flight] Expose registerServerReference from the client builds (#32534) This is used to register Server References that exist in the current environment but also exists in the server it might call into. Such as a remote server. If the value comes from the remote server in the first place then this is called automatically to ensure that you can pass a reference back to where it came from - even if the `serverModuleMap` option is used. This was already the case when `serverModuleMap` wasn't passed. This is how you can pass server references back to the server. However, when we added `serverModuleMap` that pass was skipped because we were getting real functions instead of proxies. For functions that wasn't yet passed from the remote server to the current server, we can register them eagerly just like we do for `import('/server').registerServerReference()`. You can now also do this with `import('/client').registerServerReference()`. We could make them shared so you only have to do this once but it might not be possible to pass to the remote server and the remote server might not even be the same RSC renderer. Therefore I split them. It's up to the compiler whether it should do that or not. It has to know that any function you might call might be able to receive it. This is currently global to a specific RSC renderer. --- .../react-client/src/ReactFlightClient.js | 21 +++++++++- .../src/ReactFlightReplyClient.js | 26 ++++++++---- .../src/client/ReactFlightDOMClientBrowser.js | 13 +++--- .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientBrowser.js | 2 + .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientBrowser.js | 13 +++--- .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/__tests__/ReactFlightDOMEdge-test.js | 42 +++++++++++++++++++ .../__tests__/ReactFlightDOMReplyEdge-test.js | 29 ++++++++++++- .../src/client/ReactFlightDOMClientBrowser.js | 13 +++--- .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/ReactFlightReplyServer.js | 6 ++- 16 files changed, 141 insertions(+), 38 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0eaf513a67..3234814952 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -64,7 +64,10 @@ import { rendererPackageName, } from './ReactFlightClientConfig'; -import {createBoundServerReference} from './ReactFlightReplyClient'; +import { + createBoundServerReference, + registerBoundServerReference, +} from './ReactFlightReplyClient'; import {readTemporaryReference} from './ReactFlightTemporaryReferences'; @@ -1096,7 +1099,14 @@ function loadServerReference, T>( let promise: null | Thenable = preloadModule(serverReference); if (!promise) { if (!metaData.bound) { - return (requireModule(serverReference): any); + const resolvedValue = (requireModule(serverReference): any); + registerBoundServerReference( + resolvedValue, + metaData.id, + metaData.bound, + response._encodeFormAction, + ); + return resolvedValue; } else { promise = Promise.resolve(metaData.bound); } @@ -1128,6 +1138,13 @@ function loadServerReference, T>( resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs); } + registerBoundServerReference( + resolvedValue, + metaData.id, + metaData.bound, + response._encodeFormAction, + ); + parentObject[key] = resolvedValue; // If this is the root object for a model reference, where `handler.value` diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 65d1129b53..3fa37cd00c 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -1125,11 +1125,12 @@ function createFakeServerFunction, T>( } } -function registerServerReference( - proxy: any, - reference: {id: ServerReferenceId, bound: null | Thenable>}, +export function registerBoundServerReference( + reference: T, + id: ServerReferenceId, + bound: null | Thenable>, encodeFormAction: void | EncodeFormActionCallback, -) { +): void { // Expose encoder for use by SSR, as well as a special bind that can be used to // keep server capabilities. if (usedWithSSR) { @@ -1147,13 +1148,22 @@ function registerServerReference( encodeFormAction, ); }; - Object.defineProperties((proxy: any), { + Object.defineProperties((reference: any), { $$FORM_ACTION: {value: $$FORM_ACTION}, $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual}, bind: {value: bind}, }); } - knownServerReferences.set(proxy, reference); + knownServerReferences.set(reference, {id, bound}); +} + +export function registerServerReference( + reference: T, + id: ServerReferenceId, + encodeFormAction?: EncodeFormActionCallback, +): ServerReference { + registerBoundServerReference(reference, id, null, encodeFormAction); + return reference; } // $FlowFixMe[method-unbinding] @@ -1258,7 +1268,7 @@ export function createBoundServerReference, T>( ); } } - registerServerReference(action, {id, bound}, encodeFormAction); + registerBoundServerReference(action, id, bound, encodeFormAction); return action; } @@ -1358,6 +1368,6 @@ export function createServerReference, T>( ); } } - registerServerReference(action, {id, bound: null}, encodeFormAction); + registerBoundServerReference(action, id, null, encodeFormAction); return action; } diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index afabc29104..9ae47e3b55 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -25,9 +25,11 @@ import { injectIntoDevTools, } from 'react-client/src/ReactFlightClient'; -import { - processReply, +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { createServerReference, + registerServerReference, } from 'react-client/src/ReactFlightReplyClient'; import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; @@ -151,12 +153,7 @@ function encodeReply( }); } -export { - createFromFetch, - createFromReadableStream, - encodeReply, - createServerReference, -}; +export {createFromFetch, createFromReadableStream, encodeReply}; if (__DEV__) { injectIntoDevTools(); diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js index 2e1c556642..75c569e5ac 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js @@ -26,6 +26,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js index 7ea840b140..3aca4a355d 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js @@ -26,6 +26,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js index 8783cbfc6a..b1fbfed08f 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js @@ -25,6 +25,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js index 8be06af9f2..b12a3a3ff4 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js @@ -21,6 +21,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function findSourceMapURL(filename: string, environmentName: string) { const devServer = parcelRequire.meta.devServer; if (devServer != null) { diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index b6b55e4586..ee319beca1 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -25,9 +25,11 @@ import { injectIntoDevTools, } from 'react-client/src/ReactFlightClient'; -import { - processReply, +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { createServerReference, + registerServerReference, } from 'react-client/src/ReactFlightReplyClient'; import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; @@ -150,12 +152,7 @@ function encodeReply( }); } -export { - createFromFetch, - createFromReadableStream, - encodeReply, - createServerReference, -}; +export {createFromFetch, createFromReadableStream, encodeReply}; if (__DEV__) { injectIntoDevTools(); diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index 509950bc65..48cb0dd4db 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -41,6 +41,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js index 2ee76fa3b4..919be523f8 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js @@ -38,6 +38,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index ba1ae3b64a..5d3af9d411 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -301,6 +301,48 @@ describe('ReactFlightDOMEdge', () => { expect(result.boundMethod()).toBe('hi, there'); }); + it('should load a server reference on a consuming server and pass it back', async () => { + function greet(name) { + return 'hi, ' + name; + } + const ServerModule = serverExports({ + greet, + }); + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + { + method: ServerModule.greet, + boundMethod: ServerModule.greet.bind(null, 'there'), + }, + webpackMap, + ), + ); + const response = ReactServerDOMClient.createFromReadableStream(stream, { + serverConsumerManifest: { + moduleMap: webpackMap, + serverModuleMap: webpackServerMap, + moduleLoading: webpackModuleLoading, + }, + }); + + const result = await response; + + expect(result.method).toBe(greet); + expect(result.boundMethod()).toBe('hi, there'); + + const body = await ReactServerDOMClient.encodeReply({ + method: result.method, + boundMethod: result.boundMethod, + }); + const replyResult = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + expect(replyResult.method).toBe(greet); + expect(replyResult.boundMethod()).toBe('hi, there'); + }); + it('should encode long string in a compact format', async () => { const testString = '"\n\t'.repeat(500) + '🙃'; const testString2 = 'hello'.repeat(400); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index 2effa9868e..7315e78c61 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -22,7 +22,7 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') { global.FormData = require('undici').FormData; } -// let serverExports; +let serverExports; let webpackServerMap; let ReactServerDOMServer; let ReactServerDOMClient; @@ -36,7 +36,7 @@ describe('ReactFlightDOMReplyEdge', () => { require('react-server-dom-webpack/server.edge'), ); const WebpackMock = require('./utils/WebpackMock'); - // serverExports = WebpackMock.serverExports; + serverExports = WebpackMock.serverExports; webpackServerMap = WebpackMock.webpackServerMap; ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); jest.resetModules(); @@ -308,4 +308,29 @@ describe('ReactFlightDOMReplyEdge', () => { expect(await decoded.a).toBe('hello'); expect(Array.from(await decoded.b)).toEqual(Array.from(buffer)); }); + + it('can pass a registered server reference', async () => { + function greet(name) { + return 'hi, ' + name; + } + const ServerModule = serverExports({ + greet, + }); + + ReactServerDOMClient.registerServerReference( + ServerModule.greet, + ServerModule.greet.$$id, + ); + + const body = await ReactServerDOMClient.encodeReply({ + method: ServerModule.greet, + boundMethod: ServerModule.greet.bind(null, 'there'), + }); + const replyResult = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + expect(replyResult.method).toBe(greet); + expect(replyResult.boundMethod()).toBe('hi, there'); + }); }); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index b6b55e4586..ee319beca1 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -25,9 +25,11 @@ import { injectIntoDevTools, } from 'react-client/src/ReactFlightClient'; -import { - processReply, +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { createServerReference, + registerServerReference, } from 'react-client/src/ReactFlightReplyClient'; import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; @@ -150,12 +152,7 @@ function encodeReply( }); } -export { - createFromFetch, - createFromReadableStream, - encodeReply, - createServerReference, -}; +export {createFromFetch, createFromReadableStream, encodeReply}; if (__DEV__) { injectIntoDevTools(); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index 509950bc65..48cb0dd4db 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -41,6 +41,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js index 22c8928432..4118ad046d 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js @@ -39,6 +39,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 4db7571bb6..7c94352f28 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -936,8 +936,10 @@ function parseModelString( // Server Reference const ref = value.slice(2); // TODO: Just encode this in the reference inline instead of as a model. - const metaData: {id: ServerReferenceId, bound: Thenable>} = - getOutlinedModel(response, ref, obj, key, createModel); + const metaData: { + id: ServerReferenceId, + bound: null | Thenable>, + } = getOutlinedModel(response, ref, obj, key, createModel); return loadServerReference( response, metaData.id, From 029e8bd618af23fbdd9efdac565ad81f7d4640d8 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 6 Mar 2025 17:12:50 +0100 Subject: [PATCH 4/5] Add Owner Stack to attribute hydration mismatches (#32538) --- .../src/__tests__/ReactDOMFizzForm-test.js | 104 +++--- .../src/__tests__/ReactDOMFizzServer-test.js | 2 +- .../__tests__/ReactDOMHydrationDiff-test.js | 313 +++++++++++++++--- .../src/__tests__/ReactDOMRoot-test.js | 42 ++- .../ReactDOMSingletonComponents-test.js | 50 ++- .../src/__tests__/ReactRenderDocument-test.js | 5 +- .../ReactServerRenderingHydration-test.js | 113 +++---- .../src/ReactFiberHydrationContext.js | 43 ++- 8 files changed, 444 insertions(+), 228 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index f64cdd8bb8..c7f52b1c68 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 5b44d9e02e..7542582528 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 2efcfd01d6..c445f458e5 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', () => { +
-