Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ function warnForPropDifference(
if (normalizedServerValue === normalizedClientValue) {
return;
}
if (propName === 'class' || propName === 'className') {
const normalizedClientClass = normalizeClassForHydration(clientValue);
const normalizedServerClass = normalizeClassForHydration(serverValue);
if (normalizedServerClass === normalizedClientClass) {
return;
}
}

serverDifferences[propName] = serverValue;
}
Expand Down Expand Up @@ -300,6 +307,12 @@ function normalizeHTML(parent: Element, html: string) {
const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;

// 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]+/;

function normalizeMarkupForTextOrAttribute(markup: mixed): string {
if (__DEV__) {
checkHtmlStringCoercion(markup);
Expand All @@ -310,6 +323,14 @@ function normalizeMarkupForTextOrAttribute(markup: mixed): string {
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
}

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(' ');
}

function checkForUnmatchedText(
serverText: string,
clientText: string | number | bigint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,35 @@ describe('ReactDOMServerHydration', () => {
]);
});

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([]);
});

it('should throw rendering portals on the server', () => {
const div = document.createElement('div');
expect(() => {
Expand Down