From 6bce3bcb96de2076bbf2a576ec268fc5eb05846f Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Tue, 26 Mar 2024 21:05:31 -0400 Subject: [PATCH] Support suppressHydrationWarning for element-ish text --- .../src/client/ReactFiberConfigDOM.js | 34 +++++++++--- ...actDOMFizzSuppressHydrationWarning-test.js | 53 +++++++++++++++++++ packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 3 +- .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../forks/ReactFeatureFlags.www-dynamic.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 10 files changed, 90 insertions(+), 8 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 450038eb70dd..725ead78ae75 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -93,6 +93,7 @@ import { enableScopeAPI, enableTrustedTypesIntegration, enableAsyncActions, + disableElementishSuppressionCheck, } from 'shared/ReactFeatureFlags'; import { HostComponent, @@ -1528,13 +1529,32 @@ export function didNotMatchHydratedTextInstance( isConcurrentMode: boolean, shouldWarnDev: boolean, ) { - if (parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - checkForUnmatchedText( - textInstance.nodeValue, - text, - isConcurrentMode, - shouldWarnDev, - ); + if (disableElementishSuppressionCheck) { + if (parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + checkForUnmatchedText( + textInstance.nodeValue, + text, + isConcurrentMode, + shouldWarnDev, + ); + } + } else { + if ( + parentProps[SUPPRESS_HYDRATION_WARNING] !== true && + // TODO: remove this hack. + // For elementish text nodes, we need to check their prop through the parent. + parentProps.children && + parentProps.children.length === 1 && + parentProps.children.props && + parentProps.children.props[SUPPRESS_HYDRATION_WARNING] !== true + ) { + checkForUnmatchedText( + textInstance.nodeValue, + text, + isConcurrentMode, + shouldWarnDev, + ); + } } } diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 3300297b77ee..eca326dda5d6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -16,6 +16,7 @@ let Scheduler; let React; let ReactDOMClient; let ReactDOMFizzServer; +let ReactFeatureFlags; let document; let writable; let container; @@ -33,6 +34,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ReactDOMClient = require('react-dom/client'); ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; @@ -784,4 +786,55 @@ describe('ReactDOMFizzServerHydrationWarning', () => { , ); }); + + // @gate enableClientRenderFallbackOnTextMismatch && !disableElementishSuppressionCheck + it('suppresses but does not fix text mismatches with suppressHydrationWarning for element-ish children', async () => { + function App({isClient}) { + // This is similar to . + // We don't toString it because you must instead provide a value prop. + const obj = { + $$typeof: Symbol.for('react.element'), + type: props => props.content, + ref: null, + key: null, + props: { + suppressHydrationWarning: true, + content: isClient ? 'Client Text' : 'Server Text', + }, + toString() { + return this.props.content; + }, + }; + + return ( +
+ {obj} +
+ ); + } + await act(() => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ Server Text +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + // Don't miss a hydration error. There should be none. + Scheduler.log(error.message); + }, + }); + await waitForAll([]); + // The text mismatch should be *silently* fixed. Even in production. + expect(getVisibleChildren(container)).toEqual( +
+ Server Text +
, + ); + }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 8f7033421cd9..da86318249c3 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -40,6 +40,8 @@ export const disableSchedulerTimeoutInWorkLoop = false; // those can be fixed. export const enableDeferRootSchedulingToMicrotask = true; +export const disableElementishSuppressionCheck = true; + // ----------------------------------------------------------------------------- // Slated for removal in the future (significant effort) // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 582cb140107e..b83de7a95bf2 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -14,6 +14,7 @@ import typeof * as DynamicExportsType from './ReactFeatureFlags.native-fb-dynami // Re-export dynamic flags from the internal module. // Intentionally using * because this import is compiled to a `require` call. import * as dynamicFlagsUntyped from 'ReactNativeInternalFeatureFlags'; +import {disableElementishSuppressionCheck} from 'shared/ReactFeatureFlags'; const dynamicFlags: DynamicExportsType = (dynamicFlagsUntyped: any); // We destructure each value before re-exporting to avoid a dynamic look-up on @@ -93,7 +94,7 @@ export const disableClientCache = true; export const enableServerComponentKeys = true; export const enableServerComponentLogs = true; - +export const disableElementishSuppressionCheck = true; // TODO: Roll out with GK. Don't keep as dynamic flag for too long, though, // because JSX is an extremely hot path. export const enableRefAsProp = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 46f96b7c9d44..7383b743655c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -105,6 +105,7 @@ export const enableNewBooleanProps = true; export const enableTransitionTracing = false; export const enableDO_NOT_USE_disableStrictPassiveEffect = false; export const passChildrenWhenCloningPersistedNodes = false; +export const disableElementishSuppressionCheck = true; // Profiling Only export const enableProfilerTimer = __PROFILE__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index c0430218efdf..c15b2b0f276c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -80,6 +80,7 @@ export const disableClientCache = true; export const enableServerComponentKeys = true; export const enableServerComponentLogs = true; export const enableInfiniteRenderLoopDetection = false; +export const disableElementishSuppressionCheck = true; // TODO: This must be in sync with the main ReactFeatureFlags file because // the Test Renderer's value must be the same as the one used by the diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index f307540b20c2..f4596e3991cb 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -90,6 +90,7 @@ export const enableReactTestRendererWarning = false; export const disableLegacyMode = false; export const enableBigIntSupport = false; +export const disableElementishSuppressionCheck = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index cb499f31d703..61ffc6def641 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -92,6 +92,7 @@ export const enableReactTestRendererWarning = false; export const disableLegacyMode = false; export const enableBigIntSupport = true; +export const disableElementishSuppressionCheck = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 89ef7fae0858..98a3470c80b6 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -33,6 +33,7 @@ export const enableRefAsProp = __VARIANT__; export const enableClientRenderFallbackOnTextMismatch = __VARIANT__; export const enableNewBooleanProps = __VARIANT__; export const enableRetryLaneExpiration = __VARIANT__; +export const disableElementishSuppressionCheck = !__VARIANT__; export const retryLaneExpirationMs = 5000; export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 8902b5efac7c..a4ec49b25b0e 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -40,6 +40,7 @@ export const { enableRefAsProp, enableNewBooleanProps, enableClientRenderFallbackOnTextMismatch, + disableElementishSuppressionCheck, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build.