Skip to content

Conversation

@teamleaderleo
Copy link

@teamleaderleo teamleaderleo commented Oct 22, 2025

Fixes #34939

This PR normalizes className values as token sets during DEV hydration comparison, eliminating false-positive warnings when server and client produce semantically identical classes that differ only in whitespace, token order, or duplicates.

Changes:

  • Added normalizeClassForHydration(markup: mixed): string to normalize class strings
  • Modified warnForPropDifference to compare normalized forms for class/className props
  • Added tests covering whitespace/order/duplicate/CRLF scenarios
  • No production behavior change (DEV-only)

Rationale:

As per the HTML spec, class is an unordered set of space-separated tokens. React's current class/className string comparison produces false positives that don't represent actual DOM differences.

Specs:

Precedent:

  • 44c32fc (2017) - React already normalizes CR/LF in warnForPropDifference to handle HTML parser behavior. This PR extends that pattern to handle class attribute's token-set semantics.

Summary

Motivation:

React currently throws hydration warnings when server and client className values are semantically identical but differ only in representation (i.e. differing only by whitespace, token order, or duplicates). This is a false positive: the DOM result is identical as per the HTML spec, which defines class as an unordered set of tokens.

This issue has become increasingly common since the initial hydration normalization code (44c32fc) was written 8 years ago (2017):

  • Tailwind CSS generates many utility classes, encouraging multiline formatting.
  • Component libraries (shadcn/ui, Radix UI) use class-merging utilities (clsx, cn) that may reorder tokens.
  • Multiline className template literals (encouraged by long Tailwind utility lists) expose CRLF line-ending differences on Windows.
  • Build pipeline differences (minification, bundling, transpilation) may handle whitespace inconsistently between environments.

This PR eliminates these false positives while preserving React's ability to catch genuine hydration mismatches.

Implementation

Added HTML_SPACE_CLASS_SEPARATOR constant:

// HTML: `class` is a space-separated token list split on ASCII whitespace
// (TAB U+0009, LF U+000A, FF U+000C, CR U+000D, SPACE U+0020).
// Specs: https://infra.spec.whatwg.org/#ascii-whitespace
//        https://html.spec.whatwg.org/multipage/dom.html#global-attributes
const HTML_SPACE_CLASS_SEPARATOR = /[ \t\n\f\r]+/;

Added normalization function:

function normalizeClassForHydration(markup: mixed): string {
  const s = normalizeMarkupForTextOrAttribute(markup);
  const tokens = s.trim().split(HTML_SPACE_CLASS_SEPARATOR).filter(Boolean);
  const unique = Array.from(new Set(tokens));
  unique.sort();
  return unique.join(' ');
}

Early return in warnForPropDifference for normalized matches:

if (propName === 'class' || propName === 'className') {
  const normalizedClientClass = normalizeClassForHydration(clientValue);
  const normalizedServerClass = normalizeClassForHydration(serverValue);
  if (normalizedServerClass === normalizedClientClass) {
    return;
  }
}

How did you test this change?

Added tests:

it('should not warn when class differs only by whitespace/order/duplicates', async () => {
  const element = document.createElement('div');
  element.innerHTML = ReactDOMServer.renderToString(
    <div className="foo   bar  bar" />,
  );

  await act(() => {
    ReactDOMClient.hydrateRoot(element, <div className="bar foo" />);
  });

  assertConsoleErrorDev([]);
});

it('should not warn for class with CRLF (Windows line endings)', async () => {
  const element = document.createElement('div');
  element.innerHTML = ReactDOMServer.renderToString(
    <div className={'flex\r\nitems-center'} />,
  );

  await act(() => {
    ReactDOMClient.hydrateRoot(
      element,
      <div className="flex items-center" />,
    );
  });

  assertConsoleErrorDev([]);
});

Test runs:

  • yarn test react-dom - all tests pass
image
  • yarn test react-dom --prod - all tests pass
image
  • Verified in production app via patch-package: eliminates false positives while preserving real mismatch warnings

Verified that genuine mismatches still warn:

  • Different tokens ("flex" vs "grid") - still warns
  • Missing/extra tokens - still warns

HTML defines `class` as a set of space-separated tokens. During hydration, server and client may differ only by whitespace, duplicates, or order. In DEV, treat both sides as equal after normalizing (split on ASCII whitespace, dedupe + sort) and early-return from warnForPropDifference when equal.

- Add normalizeClassForHydration()
- Compare canonical forms only for class/className (DEV only)
- Tests: no warning for whitespace/order/dupes/CRLF between tokens
- No production behavior change

Specs:
- ASCII whitespace: https://infra.spec.whatwg.org/#ascii-whitespace
- class token list: https://html.spec.whatwg.org/multipage/dom.html#global-attributes

Precedent:
- 44c32fc (2017) CR/LF normalization in warnForPropDifference
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Dev hydration false-positive for equivalent class token sets

1 participant