-
Notifications
You must be signed in to change notification settings - Fork 49.7k
Description
Proposed fix in PR: #34940
React version: 19.x (also reproducible on current main)
Steps To Reproduce
- Server-render markup where
classvalues are token-equivalent but differ by whitespace/order/duplicates. - Hydrate on the client with the same tokens in a different order/spacing.
Minimal reproduction:
import * as React from 'react'; import * as ReactDOMClient from 'react-dom/client'; import * as ReactDOMServer from 'react-dom/server'; const container = document.createElement('div'); container.innerHTML = ReactDOMServer.renderToString( <main className="foo bar bar" /> ); ReactDOMClient.hydrateRoot( container, <main className="bar foo" />, { onRecoverableError: () => {} } // surfaces the warning in DEV );
CRLF variant:
container.innerHTML = ReactDOMServer.renderToString( <main className="flex\r\nitems-center" /> ); ReactDOMClient.hydrateRoot(container, <main className="flex items-center" />);
Link to code example
- Order/dupes/whitespace: https://codesandbox.io/p/sandbox/rprwrz?file=%2Findex.html&case=order
- CRLF between tokens: https://codesandbox.io/p/sandbox/rprwrz?file=%2Findex.html&case=crlf
Open the Console to see the hydration warning (DEV build).
The current behavior
In DEV, React logs a hydration attribute mismatch warning when the server and client class strings differ only by whitespace, token order, or duplicate tokens, even though they represent the same token set.
The expected behavior
No warning. Per HTML, class is a set of space-separated tokens, so differences in whitespace, order, or duplicate tokens should be considered equivalent during hydration diffing in DEV.
Specs:
- ASCII whitespace (TAB/LF/FF/CR/SPACE): https://infra.spec.whatwg.org/#ascii-whitespace
classis a space-separated token set: https://html.spec.whatwg.org/multipage/dom.html#global-attributes
Why this matters:
This false positive can be commonly triggered by:
- Windows line endings (CRLF) in multiline template literals
- Class-merging utilities (clsx, cn) with different evaluation order
- Different bundler/minifier behavior between server and client
Proposed fix:
Normalize both values before comparison (DEV-only):
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(' '); }
This proposal:
- Handles whitespace per HTML spec (ASCII whitespace characters)
- Deduplicates tokens (matches DOMTokenList behavior)
- Sorts for order-independence (classes are unordered)
- Uses default lexicographic sort (UTF-16 code unit order), which is deterministic and locale-independent
- Reuses existing
normalizeMarkupForTextOrAttributefor consistency
What wouldn’t be suppressed
- Missing/extra class tokens
- Non-ASCII whitespace that changes tokenization (e.g., NBSP)
- Any difference that affects layout/behavior
Notes: This is DEV-only (warning path). Production behavior is unaffected. A small DEV change to compare normalized token sets would avoid this false positive.
This should preserve React's ability to catch genuine mismatches while eliminating false positives for token-equivalent classes.
Precedent:
React already normalizes attributes during hydration for HTML parser behavior (see 44c32fc - CRLF normalization from 2017). This extends that pattern to handle class's token-set semantics.