Skip to content

Commit 96ee8ba

Browse files
committed
Increaese the priority to user blocking for every next discrete boundary
1 parent 29ed08e commit 96ee8ba

File tree

4 files changed

+113
-12
lines changed

4 files changed

+113
-12
lines changed

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

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
166166

167167
container.innerHTML = finalHTML;
168168

169-
let span = container.getElementsByTagName('span')[3];
169+
let spanD = container.getElementsByTagName('span')[3];
170170

171171
suspend = true;
172172

@@ -179,7 +179,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
179179
expect(Scheduler).toHaveYielded([]);
180180

181181
// This click target cannot be hydrated yet because it's suspended.
182-
let result = dispatchClickEvent(span);
182+
let result = dispatchClickEvent(spanD);
183183

184184
expect(Scheduler).toHaveYielded(['App']);
185185

@@ -198,4 +198,96 @@ describe('ReactDOMServerSelectiveHydration', () => {
198198

199199
document.body.removeChild(container);
200200
});
201+
202+
it('hydrates at higher pri for secondary discrete events', async () => {
203+
let suspend = false;
204+
let resolve;
205+
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
206+
207+
function Child({text}) {
208+
if ((text === 'A' || text === 'D') && suspend) {
209+
throw promise;
210+
}
211+
Scheduler.unstable_yieldValue(text);
212+
return (
213+
<span
214+
onClick={e => {
215+
e.preventDefault();
216+
Scheduler.unstable_yieldValue('Clicked ' + text);
217+
}}>
218+
{text}
219+
</span>
220+
);
221+
}
222+
223+
function App() {
224+
Scheduler.unstable_yieldValue('App');
225+
return (
226+
<div>
227+
<Suspense fallback="Loading...">
228+
<Child text="A" />
229+
</Suspense>
230+
<Suspense fallback="Loading...">
231+
<Child text="B" />
232+
</Suspense>
233+
<Suspense fallback="Loading...">
234+
<Child text="C" />
235+
</Suspense>
236+
<Suspense fallback="Loading...">
237+
<Child text="D" />
238+
</Suspense>
239+
</div>
240+
);
241+
}
242+
243+
let finalHTML = ReactDOMServer.renderToString(<App />);
244+
245+
expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
246+
247+
let container = document.createElement('div');
248+
// We need this to be in the document since we'll dispatch events on it.
249+
document.body.appendChild(container);
250+
251+
container.innerHTML = finalHTML;
252+
253+
let spanA = container.getElementsByTagName('span')[0];
254+
let spanC = container.getElementsByTagName('span')[2];
255+
let spanD = container.getElementsByTagName('span')[3];
256+
257+
suspend = true;
258+
259+
// A and D will be suspended. We'll click on D which should take
260+
// priority, after we unsuspend.
261+
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
262+
root.render(<App />);
263+
264+
// Nothing has been hydrated so far.
265+
expect(Scheduler).toHaveYielded([]);
266+
267+
// This click target cannot be hydrated yet because the first is Suspended.
268+
dispatchClickEvent(spanA);
269+
dispatchClickEvent(spanC);
270+
dispatchClickEvent(spanD);
271+
272+
expect(Scheduler).toHaveYielded(['App']);
273+
274+
suspend = false;
275+
resolve();
276+
await promise;
277+
278+
// After the click, we should prioritize D and the Click first,
279+
// and only after that render A and C.
280+
expect(Scheduler).toFlushAndYield([
281+
'A',
282+
'Clicked A',
283+
'C',
284+
'Clicked C',
285+
'D',
286+
'Clicked D',
287+
// B should render last since it wasn't clicked.
288+
'B',
289+
]);
290+
291+
document.body.removeChild(container);
292+
});
201293
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
flushPassiveEffects,
4141
IsThisRendererActing,
4242
attemptSynchronousHydration,
43-
attemptHydration,
43+
attemptUserBlockingHydration,
4444
} from 'react-reconciler/inline.dom';
4545
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
4646
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -78,7 +78,7 @@ import {restoreControlledState} from './ReactDOMComponent';
7878
import {dispatchEvent} from '../events/ReactDOMEventListener';
7979
import {
8080
setAttemptSynchronousHydration,
81-
setAttemptHydration,
81+
setAttemptUserBlockingHydration,
8282
} from '../events/ReactDOMEventReplaying';
8383
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
8484
import {
@@ -90,7 +90,7 @@ import {
9090
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
9191

9292
setAttemptSynchronousHydration(attemptSynchronousHydration);
93-
setAttemptHydration(attemptHydration);
93+
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
9494

9595
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
9696

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

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

40-
let attemptHydration: (fiber: Object) => void;
40+
let attemptUserBlockingHydration: (fiber: Object) => void;
4141

42-
export function setAttemptHydration(fn: (fiber: Object) => void) {
43-
attemptHydration = fn;
42+
export function setAttemptUserBlockingHydration(fn: (fiber: Object) => void) {
43+
attemptUserBlockingHydration = fn;
4444
}
4545

4646
// TODO: Upgrade this definition once we're on a newer version of Flow that
@@ -442,6 +442,12 @@ function replayUnblockedEvents() {
442442
let nextDiscreteEvent = queuedDiscreteEvents[0];
443443
if (nextDiscreteEvent.blockedOn !== null) {
444444
// We're still blocked.
445+
// Increase the priority of this boundary to unblock
446+
// the next discrete event.
447+
let fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn);
448+
if (fiber !== null) {
449+
attemptUserBlockingHydration(fiber);
450+
}
445451
break;
446452
}
447453
let nextBlockedOn = attemptToDispatchEvent(

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ import {
7878
current as ReactCurrentFiberCurrent,
7979
} from './ReactCurrentFiber';
8080
import {StrictMode} from './ReactTypeOfMode';
81-
import {Sync} from './ReactFiberExpirationTime';
81+
import {Sync, computeInteractiveExpiration} from './ReactFiberExpirationTime';
8282
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
8383
import {
8484
scheduleRefresh,
@@ -384,7 +384,8 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
384384
// If we're still blocked after this, we need to increase
385385
// the priority of any promises resolving within this
386386
// boundary so that they next attempt also has higher pri.
387-
markRetryTimeIfNotHydrated(fiber, Sync);
387+
let retryExpTime = computeInteractiveExpiration(requestCurrentTime());
388+
markRetryTimeIfNotHydrated(fiber, retryExpTime);
388389
break;
389390
}
390391
}
@@ -407,15 +408,17 @@ function markRetryTimeIfNotHydrated(fiber: Fiber, retryTime: ExpirationTime) {
407408
}
408409
}
409410

410-
export function attemptHydration(fiber: Fiber): void {
411+
export function attemptUserBlockingHydration(fiber: Fiber): void {
411412
if (fiber.tag !== SuspenseComponent) {
412413
// We ignore HostRoots here because we can't increase
413414
// their priority and they should not suspend on I/O,
414415
// since you have to wrap anything that might suspend in
415416
// Suspense.
416417
return;
417418
}
418-
// TODO
419+
let expTime = computeInteractiveExpiration(requestCurrentTime());
420+
scheduleWork(fiber, expTime);
421+
markRetryTimeIfNotHydrated(fiber, expTime);
419422
}
420423

421424
export {findHostInstance};

0 commit comments

Comments
 (0)