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

Lines changed: 453 additions & 292 deletions

‎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+
letsuspend=false;
95+
letresolve;
96+
letpromise=newPromise(resolvePromise=>(resolve=resolvePromise));
97+
letref=React.createRef();
98+
99+
functionChild(){
100+
if(suspend){
101+
throwpromise;
102+
}else{
103+
return'Hello';
104+
}
105+
}
106+
107+
functionApp(){
108+
return(
109+
<div>
110+
<Suspensefallback="Loading...">
111+
<spanref={ref}>
112+
<Child/>
113+
</span>
114+
</Suspense>
115+
</div>
116+
);
117+
}
118+
119+
// Don't suspend on the server.
120+
suspend=false;
121+
letfinalHTML=ReactDOMServer.renderToString(<App/>);
122+
123+
letcontainer=document.createElement('div');
124+
container.innerHTML=finalHTML;
125+
126+
letspan=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+
letspan2=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+
awaitpromise;
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
letsuspend=false;
95166
letpromise=newPromise(()=>{});
@@ -135,7 +206,8 @@ describe('ReactDOMServerPartialHydration', () => {
135206
suspend=true;
136207

137208
act(()=>{
138-
ReactDOM.hydrate(<App/>,container);
209+
letroot=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+
letroot=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',asyncrender=>{
106-
constelement=awaitrender(
105+
it('server renders a SuspenseList component and its children',async()=>{
106+
constexample=(
107107
<React.unstable_SuspenseList>
108108
<React.Suspensefallback="Loading A">
109109
<div>A</div>
110110
</React.Suspense>
111111
<React.Suspensefallback="Loading B">
112112
<div>B</div>
113113
</React.Suspense>
114-
</React.unstable_SuspenseList>,
114+
</React.unstable_SuspenseList>
115115
);
116+
constelement=awaitserverRender(example);
116117
constparent=element.parentNode;
117118
constdivA=parent.children[0];
118119
expect(divA.tagName).toBe('DIV');
119120
expect(divA.textContent).toBe('A');
120121
constdivB=parent.children[1];
121122
expect(divB.tagName).toBe('DIV');
122123
expect(divB.textContent).toBe('B');
124+
125+
ReactTestUtils.act(()=>{
126+
constroot=ReactDOM.unstable_createSyncRoot(parent,{hydrate: true});
127+
root.render(example);
128+
});
129+
130+
constparent2=element.parentNode;
131+
constdivA2=parent2.children[0];
132+
constdivB2=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
typeMeasurementPhase=
@@ -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
importtype{UpdateQueue}from'./ReactUpdateQueue';
2525
importtype{ContextDependency}from'./ReactFiberNewContext';
2626
importtype{HookType}from'./ReactFiberHooks';
27+
importtype{SuspenseInstance}from'./ReactFiberHostConfig';
2728

2829
importinvariantfrom'shared/invariant';
2930
importwarningWithoutStackfrom'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
returnfiber;
844846
}
845847

848+
exportfunctioncreateFiberFromDehydratedFragment(
849+
dehydratedNode: SuspenseInstance,
850+
): Fiber{
851+
constfiber=createFiber(DehydratedFragment,null,null,NoMode);
852+
fiber.stateNode=dehydratedNode;
853+
returnfiber;
854+
}
855+
846856
exportfunctioncreateFiberFromPortal(
847857
portal: ReactPortal,
848858
mode: TypeOfMode,

0 commit comments

Comments
 (0)