Skip to content

Commit

Permalink
Add favorSafetyOverHydrationPerf flag
Browse files Browse the repository at this point in the history
If false, this ignores text comparison checks during hydration at the risk of
privacy safety.
  • Loading branch information
sebmarkbage committed Mar 27, 2024
1 parent 2ec2aae commit b41df97
Show file tree
Hide file tree
Showing 15 changed files with 216 additions and 82 deletions.
2 changes: 2 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4423,6 +4423,7 @@ describe('ReactDOMFizzServer', () => {
);
});

// @gate favorSafetyOverHydrationPerf
it('#24384: Suspending should halt hydration warnings but still emit hydration warnings after unsuspending if mismatches are genuine', async () => {
const makeApp = () => {
let resolve, resolved;
Expand Down Expand Up @@ -4506,6 +4507,7 @@ describe('ReactDOMFizzServer', () => {
await waitForAll([]);
});

// @gate favorSafetyOverHydrationPerf
it('only warns once on hydration mismatch while within a suspense boundary', async () => {
const originalConsoleError = console.error;
const mockError = jest.fn();
Expand Down
2 changes: 2 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6446,6 +6446,7 @@ body {
);
});

// @gate favorSafetyOverHydrationPerf
it('retains styles even when a new html, head, and/body mount', async () => {
await act(() => {
const {pipe} = renderToPipeableStream(
Expand Down Expand Up @@ -8230,6 +8231,7 @@ background-color: green;
]);
});

// @gate favorSafetyOverHydrationPerf
it('can render a title before a singleton even if that singleton clears its contents', async () => {
await act(() => {
const {pipe} = renderToPipeableStream(
Expand Down
189 changes: 131 additions & 58 deletions packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,30 +80,55 @@ describe('ReactDOMServerHydration', () => {
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content.",
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content.",
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<Mismatch isClient={true}>
<div className="parent">
<main className="child">
+ client
- server
]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
https://react.dev/link/hydration-mismatch
<Mismatch isClient={true}>
<div className="parent">
<main className="child">
+ client
- server
]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
<Mismatch isClient={true}>
<div className="parent">
<main className="child">
+ client
- server
",
]
`);
}
});

// @gate __DEV__
Expand All @@ -120,29 +145,53 @@ describe('ReactDOMServerHydration', () => {
}

/* eslint-disable no-irregular-whitespace */
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content.",
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content.",
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
https://react.dev/link/hydration-mismatch
<Mismatch isClient={true}>
<div>
+ This markup contains an nbsp entity:   client text
- This markup contains an nbsp entity:   server text
]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
<Mismatch isClient={true}>
<div>
+ This markup contains an nbsp entity:   client text
- This markup contains an nbsp entity:   server text
]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<Mismatch isClient={true}>
<div>
+ This markup contains an nbsp entity:   client text
- This markup contains an nbsp entity:   server text
",
]
`);
}
/* eslint-enable no-irregular-whitespace */
});

Expand Down Expand Up @@ -549,29 +598,53 @@ describe('ReactDOMServerHydration', () => {
function Mismatch({isClient}) {
return <div className="parent">{isClient && 'only'}</div>;
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content.",
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content.",
"Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
<Mismatch isClient={true}>
<div className="parent">
+ only
-
]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
- A server/client branch \`if (typeof window !== 'undefined')\`.
- Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
https://react.dev/link/hydration-mismatch
<Mismatch isClient={true}>
<div className="parent">
+ only
-
]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
<Mismatch isClient={true}>
<div className="parent">
+ only
-
",
]
`);
}
});

// @gate __DEV__
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3816,6 +3816,7 @@ describe('ReactDOMServerPartialHydration', () => {
);
});

// @gate favorSafetyOverHydrationPerf
it("falls back to client rendering when there's a text mismatch (direct text child)", async () => {
function DirectTextChild({text}) {
return <div>{text}</div>;
Expand Down Expand Up @@ -3845,6 +3846,7 @@ describe('ReactDOMServerPartialHydration', () => {
]);
});

// @gate favorSafetyOverHydrationPerf
it("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => {
function Sibling() {
return 'Sibling';
Expand Down
29 changes: 21 additions & 8 deletions packages/react-dom/src/__tests__/ReactRenderDocument-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ describe('rendering React components at document', () => {
);
const testDocument = getTestDocument(markup);

const favorSafetyOverHydrationPerf = gate(
flags => flags.favorSafetyOverHydrationPerf,
);
expect(() => {
ReactDOM.flushSync(() => {
ReactDOMClient.hydrateRoot(
Expand All @@ -291,19 +294,29 @@ describe('rendering React components at document', () => {
);
});
}).toErrorDev(
[
'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
],
favorSafetyOverHydrationPerf
? [
'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
]
: [
"Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
],
{
withoutStack: 1,
},
);

assertLog([
"Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
'Log recoverable error: There was an error while hydrating.',
]);
expect(testDocument.body.innerHTML).toBe('Hello world');
assertLog(
favorSafetyOverHydrationPerf
? [
"Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.",
'Log recoverable error: There was an error while hydrating.',
]
: [],
);
expect(testDocument.body.innerHTML).toBe(
favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world',
);
});

it('should render w/ no markup to full document', async () => {
Expand Down
Loading

0 comments on commit b41df97

Please sign in to comment.