Repository: facebook/react
Pull request: Fix suppressHydrationWarning not working in production

Fixes https://github.com/facebook/react/issues/24270.

We intended `suppressHydrationWarning` to be checked in production as an escape hatch in 18 and prevent mismatch errors. However, the logic mistakingly only read it in development mode. I've added regression tests for the lines I've changed.

I've noticed we don't patch up `dangerouslySetInnerHTML` like we do text content. (Although we do ignore its mismatch in production.) I left that as is, but added a test verifying the current behavior.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
patch: @@ -2811,4 +2811,96 @@ describe('ReactDOMFizzServer', () => {
       </ul>,
     );
   });
+
+  // @gate experimental
+  it('suppresses and fixes text mismatches with suppressHydrationWarning', async () => {
+    function App({isClient}) {
+      return (
+        <div>
+          <span
+            suppressHydrationWarning={true}
+            data-attr={isClient ? 'client-attr' : 'server-attr'}>
+            {isClient ? 'Client Text' : 'Server Text'}
+          </span>
+          <span suppressHydrationWarning={true}>{isClient ? 2 : 1}</span>
+          <span suppressHydrationWarning={true}>
+            hello,{isClient ? 'client' : 'server'}
+          </span>
+        </div>
+      );
+    }
+    await act(async () => {
+      const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+        <App isClient={false} />,
+      );
+      pipe(writable);
+    });
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <span data-attr="server-attr">Server Text</span>
+        <span>1</span>
+        <span>
+          {'hello,'}
+          {'server'}
+        </span>
+      </div>,
+    );
+    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
+      onRecoverableError(error) {
+        // Don't miss a hydration error. There should be none.
+        Scheduler.unstable_yieldValue(error.message);
+      },
+    });
+    expect(Scheduler).toFlushAndYield([]);
+    // The text mismatch should be *silently* fixed. Even in production.
+    // The attribute mismatch should be ignored and not fixed.
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <span data-attr="server-attr">Client Text</span>
+        <span>2</span>
+        <span>
+          {'hello,'}
+          {'client'}
+        </span>
+      </div>,
+    );
+  });
+
+  // @gate experimental
+  it('suppresses and does not fix html mismatches with suppressHydrationWarning', async () => {
+    function App({isClient}) {
+      return (
+        <div>
+          <p
+            suppressHydrationWarning={true}
+            dangerouslySetInnerHTML={{
+              __html: isClient ? 'Client HTML' : 'Server HTML',
+            }}
+          />
+        </div>
+      );
+    }
+    await act(async () => {
+      const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+        <App isClient={false} />,
+      );
+      pipe(writable);
+    });
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <p>Server HTML</p>
+      </div>,
+    );
+    ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
+      onRecoverableError(error) {
+        Scheduler.unstable_yieldValue(error.message);
+      },
+    });
+    expect(Scheduler).toFlushAndYield([]);
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <p>Server HTML</p>
+      </div>,
+    );
+  });
 });

filename: packages/react-dom/src/client/ReactDOMComponent.js
patch: @@ -91,7 +91,6 @@ const STYLE = 'style';
 const HTML = '__html';
 
 let warnedUnknownTags;
-let suppressHydrationWarning;
 
 let validatePropertiesInDevelopment;
 let warnForPropDifference;
@@ -875,7 +874,6 @@ export function diffHydratedProperties(
   let extraAttributeNames: Set<string>;
 
   if (__DEV__) {
-    suppressHydrationWarning = rawProps[SUPPRESS_HYDRATION_WARNING] === true;
     isCustomComponentTag = isCustomComponent(tag, rawProps);
     validatePropertiesInDevelopment(tag, rawProps);
   }
@@ -984,7 +982,7 @@ export function diffHydratedProperties(
       // TODO: Should we use domElement.firstChild.nodeValue to compare?
       if (typeof nextProp === 'string') {
         if (domElement.textContent !== nextProp) {
-          if (!suppressHydrationWarning) {
+          if (rawProps[SUPPRESS_HYDRATION_WARNING] !== true) {
             checkForUnmatchedText(
               domElement.textContent,
               nextProp,
@@ -996,7 +994,7 @@ export function diffHydratedProperties(
         }
       } else if (typeof nextProp === 'number') {
         if (domElement.textContent !== '' + nextProp) {
-          if (!suppressHydrationWarning) {
+          if (rawProps[SUPPRESS_HYDRATION_WARNING] !== true) {
             checkForUnmatchedText(
               domElement.textContent,
               nextProp,
@@ -1028,7 +1026,7 @@ export function diffHydratedProperties(
         isCustomComponentTag && enableCustomElementPropertySupport
           ? null
           : getPropertyInfo(propKey);
-      if (suppressHydrationWarning) {
+      if (rawProps[SUPPRESS_HYDRATION_WARNING] === true) {
         // Don't bother comparing. We're ignoring all these warnings.
       } else if (
         propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
@@ -1150,8 +1148,11 @@ export function diffHydratedProperties(
 
   if (__DEV__) {
     if (shouldWarnDev) {
-      // $FlowFixMe - Should be inferred as not undefined.
-      if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
+      if (
+        // $FlowFixMe - Should be inferred as not undefined.
+        extraAttributeNames.size > 0 &&
+        rawProps[SUPPRESS_HYDRATION_WARNING] !== true
+      ) {
         // $FlowFixMe - Should be inferred as not undefined.
         warnForExtraAttributes(extraAttributeNames);
       }

filename: packages/react-dom/src/client/ReactDOMHostConfig.js
patch: @@ -132,10 +132,7 @@ type SelectionInformation = {|
   selectionRange: mixed,
 |};
 
-let SUPPRESS_HYDRATION_WARNING;
-if (__DEV__) {
-  SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
-}
+const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
 
 const SUSPENSE_START_DATA = '$';
 const SUSPENSE_END_DATA = '/$';