Skip to content

Add test for SuspenseList useID bug #31412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
352 changes: 351 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMUseId-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* @emails react-core
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
*/

let JSDOM;
let React;
let ReactDOMClient;
Expand All @@ -24,6 +23,8 @@ let buffer = '';
let hasErrored = false;
let fatalError = undefined;
let waitForPaint;
let SuspenseList;
let assertConsoleErrorDev;

describe('useId', () => {
beforeEach(() => {
Expand All @@ -32,11 +33,16 @@ describe('useId', () => {
React = require('react');
ReactDOMClient = require('react-dom/client');
clientAct = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
useId = React.useId;
useState = React.useState;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.unstable_SuspenseList;
}

const InternalTestUtils = require('internal-test-utils');
waitForPaint = InternalTestUtils.waitForPaint;
Expand Down Expand Up @@ -375,6 +381,350 @@ describe('useId', () => {
`);
});

// @gate enableSuspenseList
it('Supports SuspenseList (reveal order default)', async () => {
function Baz({id, children}) {
return <span id={id}>{children}</span>;
}

function Bar({children}) {
const id = useId();
return <Baz id={id}>{children}</Baz>;
}

function Foo() {
return (
<SuspenseList>
<Bar>A</Bar>
<Bar>B</Bar>
</SuspenseList>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);

await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <Foo />);
});

expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);
});

// @gate enableSuspenseList
it('Supports SuspenseList (reveal order "together")', async () => {
function Baz({id, children}) {
return <span id={id}>{children}</span>;
}

function Bar({children}) {
const id = useId();
return <Baz id={id}>{children}</Baz>;
}

function Foo() {
return (
<SuspenseList revealOrder="together">
<Bar>A</Bar>
<Bar>B</Bar>
</SuspenseList>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);

await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <Foo />);
});

expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);
});

// @gate enableSuspenseList
it('Supports SuspenseList (reveal order "forwards")', async () => {
function Baz({id, children}) {
return <span id={id}>{children}</span>;
}

function Bar({children}) {
const id = useId();
return <Baz id={id}>{children}</Baz>;
}

function Foo() {
return (
<SuspenseList revealOrder="forwards">
<Bar>A</Bar>
<Bar>B</Bar>
</SuspenseList>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);

await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <Foo />);
});

// TODO: this is a bug with useID and SuspenseList revealOrder "forwards"
assertConsoleErrorDev(
[
`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

<Foo>
<SuspenseList revealOrder="forwards">
<Bar>
<Baz id=":R0:">
<span
+ id=":R0:"
- id=":R1:"
>
+ A
<Bar>
<Baz id=":R1:">
<span
+ id=":R1:"
- id=":R2:"
>
+ B
`,
],
{withoutStack: true},
);

expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);
});

// @gate enableSuspenseList
it('Supports SuspenseList (reveal order "backwards")', async () => {
function Baz({id, children}) {
return <span id={id}>{children}</span>;
}

function Bar({children}) {
const id = useId();
return <Baz id={id}>{children}</Baz>;
}

function Foo() {
return (
<SuspenseList revealOrder="backwards">
<Bar>A</Bar>
<Bar>B</Bar>
</SuspenseList>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);

if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
// TODO: this is a bug with useID and SuspenseList revealOrder "backwards"
await expect(async () => {
await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <Foo />);
});
}).rejects.toThrowError(
`Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.`,
);

expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":r1:"
>
A
</span>
<span
id=":r0:"
>
B
</span>
</div>
`);
} else {
await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <Foo />);
});

// TODO: this seems like a bug when `favorSafetyOverHydrationPerf` is false?
assertConsoleErrorDev(
[
`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

<Foo>
<SuspenseList revealOrder="backwards">
<Bar>
<Bar>
<Baz id=":R1:">
<span id=":R1:">
+ B
- A
`,
],
{withoutStack: true},
);

expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<span
id=":R1:"
>
A
</span>
<span
id=":R2:"
>
B
</span>
</div>
`);
}
});

it('basic incremental hydration', async () => {
function App() {
return (
Expand Down
Loading