Skip to content

Commit 50addf4

Browse files
authored
Refactor Partial Hydration (#16346)
* Move dehydrated to be child of regular SuspenseComponent We now store the comment node on SuspenseState instead and that indicates that this SuspenseComponent is still dehydrated. We also store a child but that is only used to represent the DOM node for deletions and getNextHostSibling. * Move logic from DehydratedSuspenseComponent to SuspenseComponent Forked based on SuspenseState.dehydrated instead. * Retry logic for dehydrated boundary We can now simplify the logic for retrying dehydrated boundaries without hydrating. This is becomes simply a reconciliation against the dehydrated fragment which gets deleted, and the new children gets inserted. * Remove dehydrated from throw Instead we use the regular Suspense path. To save code, we attach retry listeners in the commit phase even though technically we don't have to. * Pop to nearest Suspense I think this is right...? * Popping hydration state should skip past the dehydrated instance * Split mount from update and special case suspended second pass The DidCapture flag isn't used consistently in the same way. We need further refactor for this. * Reorganize update path If we remove the dehydration status in the first pass and then do a second pass because we suspended, then we need to continue as if it didn't previously suspend. Since there is no fragment child etc. However, we must readd the deletion. * Schedule context work on the boundary and not the child * Warn for Suspense hydration in legacy mode It does a two pass render that client renders the content. * Rename DehydratedSuspenseComponent -> DehydratedFragment This now doesn't represent a suspense boundary itself. Its parent does. This Fiber represents the fragment around the dehydrated content. * Refactor returns Avoids the temporary mutable variables. I kept losing track of them. * Add a comment explaining the type. Placing it in the type since that's the central point as opposed to spread out.
1 parent c203471 commit 50addf4

15 files changed

+453
-292
lines changed

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,77 @@ describe('ReactDOMServerPartialHydration', () => {
9090
expect(ref.current).toBe(span);
9191
});
9292

93+
it('warns and replaces the boundary content in legacy mode', async () => {
94+
let suspend = false;
95+
let resolve;
96+
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
97+
let ref = React.createRef();
98+
99+
function Child() {
100+
if (suspend) {
101+
throw promise;
102+
} else {
103+
return 'Hello';
104+
}
105+
}
106+
107+
function App() {
108+
return (
109+
<div>
110+
<Suspense fallback="Loading...">
111+
<span ref={ref}>
112+
<Child />
113+
</span>
114+
</Suspense>
115+
</div>
116+
);
117+
}
118+
119+
// Don't suspend on the server.
120+
suspend = false;
121+
let finalHTML = ReactDOMServer.renderToString(<App />);
122+
123+
let container = document.createElement('div');
124+
container.innerHTML = finalHTML;
125+
126+
let span = container.getElementsByTagName('span')[0];
127+
128+
// On the client we try to hydrate.
129+
suspend = true;
130+
expect(() => {
131+
act(() => {
132+
ReactDOM.hydrate(<App />, container);
133+
});
134+
}).toWarnDev(
135+
'Warning: Cannot hydrate Suspense in legacy mode. Switch from ' +
136+
'ReactDOM.hydrate(element, container) to ' +
137+
'ReactDOM.unstable_createSyncRoot(container, { hydrate: true })' +
138+
'.render(element) or remove the Suspense components from the server ' +
139+
'rendered components.' +
140+
'\n in Suspense (at **)' +
141+
'\n in div (at **)' +
142+
'\n in App (at **)',
143+
);
144+
145+
// We're now in loading state.
146+
expect(container.textContent).toBe('Loading...');
147+
148+
let span2 = container.getElementsByTagName('span')[0];
149+
// This is a new node.
150+
expect(span).not.toBe(span2);
151+
expect(ref.current).toBe(span2);
152+
153+
// Resolving the promise should render the final content.
154+
suspend = false;
155+
resolve();
156+
await promise;
157+
Scheduler.unstable_flushAll();
158+
jest.runAllTimers();
159+
160+
// We should now have hydrated with a ref on the existing span.
161+
expect(container.textContent).toBe('Hello');
162+
});
163+
93164
it('can insert siblings before the dehydrated boundary', () => {
94165
let suspend = false;
95166
let promise = new Promise(() => {});
@@ -135,7 +206,8 @@ describe('ReactDOMServerPartialHydration', () => {
135206
suspend = true;
136207

137208
act(() => {
138-
ReactDOM.hydrate(<App />, container);
209+
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
210+
root.render(<App />);
139211
});
140212

141213
expect(container.firstChild.firstChild.tagName).not.toBe('DIV');
@@ -191,7 +263,8 @@ describe('ReactDOMServerPartialHydration', () => {
191263
// hydrating anyway.
192264
suspend = true;
193265
act(() => {
194-
ReactDOM.hydrate(<App />, container);
266+
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
267+
root.render(<App />);
195268
});
196269

197270
expect(container.firstChild.children[1].textContent).toBe('Middle');

packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function initModules() {
3737
};
3838
}
3939

40-
const {resetModules, serverRender, itRenders} = ReactDOMServerIntegrationUtils(
40+
const {resetModules, serverRender} = ReactDOMServerIntegrationUtils(
4141
initModules,
4242
);
4343

@@ -102,23 +102,35 @@ describe('ReactDOMServerSuspense', () => {
102102
);
103103
});
104104

105-
itRenders('a SuspenseList component and its children', async render => {
106-
const element = await render(
105+
it('server renders a SuspenseList component and its children', async () => {
106+
const example = (
107107
<React.unstable_SuspenseList>
108108
<React.Suspense fallback="Loading A">
109109
<div>A</div>
110110
</React.Suspense>
111111
<React.Suspense fallback="Loading B">
112112
<div>B</div>
113113
</React.Suspense>
114-
</React.unstable_SuspenseList>,
114+
</React.unstable_SuspenseList>
115115
);
116+
const element = await serverRender(example);
116117
const parent = element.parentNode;
117118
const divA = parent.children[0];
118119
expect(divA.tagName).toBe('DIV');
119120
expect(divA.textContent).toBe('A');
120121
const divB = parent.children[1];
121122
expect(divB.tagName).toBe('DIV');
122123
expect(divB.textContent).toBe('B');
124+
125+
ReactTestUtils.act(() => {
126+
const root = ReactDOM.unstable_createSyncRoot(parent, {hydrate: true});
127+
root.render(example);
128+
});
129+
130+
const parent2 = element.parentNode;
131+
const divA2 = parent2.children[0];
132+
const divB2 = parent2.children[1];
133+
expect(divA).toBe(divA2);
134+
expect(divB).toBe(divB2);
123135
});
124136
});

packages/react-reconciler/src/ReactDebugFiberPerf.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
ContextConsumer,
2222
Mode,
2323
SuspenseComponent,
24-
DehydratedSuspenseComponent,
2524
} from 'shared/ReactWorkTags';
2625

2726
type MeasurementPhase =
@@ -317,8 +316,7 @@ export function stopFailedWorkTimer(fiber: Fiber): void {
317316
}
318317
fiber._debugIsCurrentlyTiming = false;
319318
const warning =
320-
fiber.tag === SuspenseComponent ||
321-
fiber.tag === DehydratedSuspenseComponent
319+
fiber.tag === SuspenseComponent
322320
? 'Rendering was suspended'
323321
: 'An error was thrown inside this error boundary';
324322
endFiberMark(fiber, null, warning);

packages/react-reconciler/src/ReactFiber.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
2424
import type {UpdateQueue} from './ReactUpdateQueue';
2525
import type {ContextDependency} from './ReactFiberNewContext';
2626
import type {HookType} from './ReactFiberHooks';
27+
import type {SuspenseInstance} from './ReactFiberHostConfig';
2728

2829
import invariant from 'shared/invariant';
2930
import warningWithoutStack from 'shared/warningWithoutStack';
@@ -48,6 +49,7 @@ import {
4849
Profiler,
4950
SuspenseComponent,
5051
SuspenseListComponent,
52+
DehydratedFragment,
5153
FunctionComponent,
5254
MemoComponent,
5355
SimpleMemoComponent,
@@ -843,6 +845,14 @@ export function createFiberFromHostInstanceForDeletion(): Fiber {
843845
return fiber;
844846
}
845847

848+
export function createFiberFromDehydratedFragment(
849+
dehydratedNode: SuspenseInstance,
850+
): Fiber {
851+
const fiber = createFiber(DehydratedFragment, null, null, NoMode);
852+
fiber.stateNode = dehydratedNode;
853+
return fiber;
854+
}
855+
846856
export function createFiberFromPortal(
847857
portal: ReactPortal,
848858
mode: TypeOfMode,

0 commit comments

Comments
 (0)