Skip to content

Commit 29ed08e

Browse files
committed
Increase retryTime for increased priority dehydrated boundaries
1 parent 3694a3b commit 29ed08e

File tree

4 files changed

+133
-2
lines changed

4 files changed

+133
-2
lines changed

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,88 @@ describe('ReactDOMServerSelectiveHydration', () => {
114114

115115
document.body.removeChild(container);
116116
});
117+
118+
it('hydrates at higher pri if sync did not work first time', async () => {
119+
let suspend = false;
120+
let resolve;
121+
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
122+
123+
function Child({text}) {
124+
if ((text === 'A' || text === 'D') && suspend) {
125+
throw promise;
126+
}
127+
Scheduler.unstable_yieldValue(text);
128+
return (
129+
<span
130+
onClick={e => {
131+
e.preventDefault();
132+
Scheduler.unstable_yieldValue('Clicked ' + text);
133+
}}>
134+
{text}
135+
</span>
136+
);
137+
}
138+
139+
function App() {
140+
Scheduler.unstable_yieldValue('App');
141+
return (
142+
<div>
143+
<Suspense fallback="Loading...">
144+
<Child text="A" />
145+
</Suspense>
146+
<Suspense fallback="Loading...">
147+
<Child text="B" />
148+
</Suspense>
149+
<Suspense fallback="Loading...">
150+
<Child text="C" />
151+
</Suspense>
152+
<Suspense fallback="Loading...">
153+
<Child text="D" />
154+
</Suspense>
155+
</div>
156+
);
157+
}
158+
159+
let finalHTML = ReactDOMServer.renderToString(<App />);
160+
161+
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
162+
163+
let container = document.createElement('div');
164+
// We need this to be in the document since we'll dispatch events on it.
165+
document.body.appendChild(container);
166+
167+
container.innerHTML = finalHTML;
168+
169+
let span = container.getElementsByTagName('span')[3];
170+
171+
suspend = true;
172+
173+
// A and D will be suspended. We'll click on D which should take
174+
// priority, after we unsuspend.
175+
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
176+
root.render(<App />);
177+
178+
// Nothing has been hydrated so far.
179+
expect(Scheduler).toHaveYielded([]);
180+
181+
// This click target cannot be hydrated yet because it's suspended.
182+
let result = dispatchClickEvent(span);
183+
184+
expect(Scheduler).toHaveYielded(['App']);
185+
186+
expect(result).toBe(true);
187+
188+
// Continuing rendering will render B next.
189+
expect(Scheduler).toFlushAndYield(['B', 'C']);
190+
191+
suspend = false;
192+
resolve();
193+
await promise;
194+
195+
// After the click, we should prioritize D and the Click first,
196+
// and only after that render A and C.
197+
expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']);
198+
199+
document.body.removeChild(container);
200+
});
117201
});

packages/react-dom/src/client/ReactDOM.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
flushPassiveEffects,
4141
IsThisRendererActing,
4242
attemptSynchronousHydration,
43+
attemptHydration,
4344
} from 'react-reconciler/inline.dom';
4445
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
4546
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -75,7 +76,10 @@ import {
7576
} from './ReactDOMComponentTree';
7677
import {restoreControlledState} from './ReactDOMComponent';
7778
import {dispatchEvent} from '../events/ReactDOMEventListener';
78-
import {setAttemptSynchronousHydration} from '../events/ReactDOMEventReplaying';
79+
import {
80+
setAttemptSynchronousHydration,
81+
setAttemptHydration,
82+
} from '../events/ReactDOMEventReplaying';
7983
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
8084
import {
8185
ELEMENT_NODE,
@@ -86,6 +90,7 @@ import {
8690
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
8791

8892
setAttemptSynchronousHydration(attemptSynchronousHydration);
93+
setAttemptHydration(attemptHydration);
8994

9095
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
9196

packages/react-dom/src/events/ReactDOMEventReplaying.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) {
3737
attemptSynchronousHydration = fn;
3838
}
3939

40+
let attemptHydration: (fiber: Object) => void;
41+
42+
export function setAttemptHydration(fn: (fiber: Object) => void) {
43+
attemptHydration = fn;
44+
}
45+
4046
// TODO: Upgrade this definition once we're on a newer version of Flow that
4147
// has this definition built-in.
4248
type PointerEvent = Event & {

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {FundamentalComponent} from 'shared/ReactWorkTags';
2020
import type {ReactNodeList} from 'shared/ReactTypes';
2121
import type {ExpirationTime} from './ReactFiberExpirationTime';
2222
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
23-
import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
23+
import type {
24+
SuspenseHydrationCallbacks,
25+
SuspenseState,
26+
} from './ReactFiberSuspenseComponent';
2427

2528
import {
2629
findCurrentHostFiber,
@@ -378,10 +381,43 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
378381
break;
379382
case SuspenseComponent:
380383
flushSync(() => scheduleWork(fiber, Sync));
384+
// If we're still blocked after this, we need to increase
385+
// the priority of any promises resolving within this
386+
// boundary so that they next attempt also has higher pri.
387+
markRetryTimeIfNotHydrated(fiber, Sync);
381388
break;
382389
}
383390
}
384391

392+
function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTime) {
393+
let suspenseState: null | SuspenseState = fiber.memoizedState;
394+
if (suspenseState !== null && suspenseState.dehydrated !== null) {
395+
if (suspenseState.retryTime < retryTime) {
396+
suspenseState.retryTime = retryTime;
397+
}
398+
}
399+
}
400+
401+
// Increases the priority of thennables when they resolve within this boundary.
402+
function markRetryTimeIfNotHydrated(fiber: Fiber, retryTime: ExpirationTime) {
403+
markRetryTimeImpl(fiber, retryTime);
404+
let alternate = fiber.alternate;
405+
if (alternate) {
406+
markRetryTimeImpl(alternate, retryTime);
407+
}
408+
}
409+
410+
export function attemptHydration(fiber: Fiber): void {
411+
if (fiber.tag !== SuspenseComponent) {
412+
// We ignore HostRoots here because we can't increase
413+
// their priority and they should not suspend on I/O,
414+
// since you have to wrap anything that might suspend in
415+
// Suspense.
416+
return;
417+
}
418+
// TODO
419+
}
420+
385421
export {findHostInstance};
386422

387423
export {findHostInstanceWithWarning};

0 commit comments

Comments
 (0)