Skip to content

Commit 371302f

Browse files
committed
Add selective hydration test
Demonstrates that selective hydration works and ids are preserved even after subsequent client updates.
1 parent 2a4e9c9 commit 371302f

File tree

1 file changed

+109
-51
lines changed

1 file changed

+109
-51
lines changed

packages/react-dom/src/__tests__/ReactDOMUseId-test.js

Lines changed: 109 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,6 @@ describe('useId', () => {
9090
}
9191
}
9292

93-
function Text({text}) {
94-
Scheduler.unstable_yieldValue(text);
95-
return text;
96-
}
97-
9893
function normalizeTreeIdForTesting(id) {
9994
const [serverClientPrefix, base32, hookIndex] = id.split(':');
10095
if (serverClientPrefix === 'r') {
@@ -359,96 +354,159 @@ describe('useId', () => {
359354
`);
360355
});
361356

362-
test('inserting a sibling before a dehydrated Suspense boundary', async () => {
357+
test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
363358
const span = React.createRef(null);
364-
function App({showMore}) {
365-
// Note: Using a dynamic array so this is treated as an insertion instead
366-
// of an update, because Fiber currently allocates a node even for
367-
// empty children.
368-
const children = [<Text key="A" text="A" />];
369-
if (showMore) {
370-
// These are client-only nodes. They aren't not included in the initial
371-
// server render.
372-
children.push(<Text key="B" text="B" />, <DivWithId key="C" />);
373-
}
374-
children.push(
359+
function App({swap}) {
360+
// Note: Using a dynamic array so these are treated as insertions and
361+
// deletions instead of updates, because Fiber currently allocates a node
362+
// even for empty children.
363+
const children = [
364+
<DivWithId key="A" />,
365+
swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
366+
<DivWithId key="D" />,
367+
];
368+
return (
369+
<>
370+
{children}
371+
<Suspense key="boundary" fallback="Loading...">
372+
<DivWithId />
373+
<span ref={span} />
374+
</Suspense>
375+
</>
376+
);
377+
}
378+
379+
await serverAct(async () => {
380+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
381+
pipe(writable);
382+
});
383+
const dehydratedSpan = container.getElementsByTagName('span')[0];
384+
await clientAct(async () => {
385+
const root = ReactDOM.hydrateRoot(container, <App />);
386+
expect(Scheduler).toFlushUntilNextPaint([]);
387+
expect(container).toMatchInlineSnapshot(`
388+
<div
389+
id="container"
390+
>
391+
<div
392+
id="101"
393+
/>
394+
<div
395+
id="1001"
396+
/>
397+
<div
398+
id="1101"
399+
/>
400+
<!--$-->
401+
<div
402+
id="110"
403+
/>
404+
<span />
405+
<!--/$-->
406+
</div>
407+
`);
408+
409+
// The inner boundary hasn't hydrated yet
410+
expect(span.current).toBe(null);
411+
412+
// Swap B for C
413+
root.render(<App swap={true} />);
414+
});
415+
// The swap should not have caused a mismatch.
416+
expect(container).toMatchInlineSnapshot(`
417+
<div
418+
id="container"
419+
>
420+
<div
421+
id="101"
422+
/>
423+
<div
424+
id="CLIENT_GENERATED_ID"
425+
/>
426+
<div
427+
id="1101"
428+
/>
429+
<!--$-->
430+
<div
431+
id="110"
432+
/>
433+
<span />
434+
<!--/$-->
435+
</div>
436+
`);
437+
// Should have hydrated successfully
438+
expect(span.current).toBe(dehydratedSpan);
439+
});
440+
441+
test('inserting/deleting siblings inside a dehydrated Suspense boundary', async () => {
442+
const span = React.createRef(null);
443+
function App({swap}) {
444+
// Note: Using a dynamic array so these are treated as insertions and
445+
// deletions instead of updates, because Fiber currently allocates a node
446+
// even for empty children.
447+
const children = [
448+
<DivWithId key="A" />,
449+
swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
450+
<DivWithId key="D" />,
451+
];
452+
return (
375453
<Suspense key="boundary" fallback="Loading...">
376-
<DivWithId />
377-
<DivWithId />
454+
{children}
378455
<span ref={span} />
379-
</Suspense>,
380-
<DivWithId key="after" />,
456+
</Suspense>
381457
);
382-
383-
return children;
384458
}
385459

386460
await serverAct(async () => {
387461
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
388462
pipe(writable);
389463
});
390-
expect(Scheduler).toHaveYielded(['A']);
391464
const dehydratedSpan = container.getElementsByTagName('span')[0];
392465
await clientAct(async () => {
393466
const root = ReactDOM.hydrateRoot(container, <App />);
394-
expect(Scheduler).toFlushUntilNextPaint(['A']);
467+
expect(Scheduler).toFlushUntilNextPaint([]);
395468
expect(container).toMatchInlineSnapshot(`
396469
<div
397470
id="container"
398471
>
399-
A
400-
<!-- -->
401472
<!--$-->
402473
<div
403-
id="110"
474+
id="101"
404475
/>
405476
<div
406-
id="1010"
477+
id="1001"
407478
/>
408-
<span />
409-
<!--/$-->
410479
<div
411-
id="11"
480+
id="1101"
412481
/>
482+
<span />
483+
<!--/$-->
413484
</div>
414485
`);
415486

416487
// The inner boundary hasn't hydrated yet
417488
expect(span.current).toBe(null);
418489

419-
// Insert another sibling before the Suspense boundary
420-
root.render(<App showMore={true} />);
490+
// Swap B for C
491+
root.render(<App swap={true} />);
421492
});
422-
expect(Scheduler).toHaveYielded([
423-
'A',
424-
'B',
425-
// The update triggers selective hydration so we render again
426-
'A',
427-
'B',
428-
]);
429-
// The insertions should not cause a mismatch.
493+
// The swap should not have caused a mismatch.
430494
expect(container).toMatchInlineSnapshot(`
431495
<div
432496
id="container"
433497
>
434-
A
435-
<!-- -->
436498
<!--$-->
437-
B
438499
<div
439-
id="CLIENT_GENERATED_ID"
500+
id="101"
440501
/>
441502
<div
442-
id="110"
503+
id="CLIENT_GENERATED_ID"
443504
/>
444505
<div
445-
id="1010"
506+
id="1101"
446507
/>
447508
<span />
448509
<!--/$-->
449-
<div
450-
id="11"
451-
/>
452510
</div>
453511
`);
454512
// Should have hydrated successfully

0 commit comments

Comments
 (0)