Prepare placeholders before timing out (#13092)
* Prepare placeholders before timing out While a tree is suspended, prepare for the timeout by pre-rendering the placeholder state. This simplifies the implementation a bit because every render now results in a completed tree. * Suspend inside an already timed out Placeholder A component should be able to suspend inside an already timed out placeholder. The time at which the placeholder committed is used as the start time for a subsequent suspend. So, if a placeholder times out after 3 seconds, and an inner placeholder has a threshold of 2 seconds, the inner placeholder will not time out until 5 seconds total have elapsed.
Andrew Clark committed
Jul 3, 2018 at 19:22 UTC
aa8266c4f7da62788156bf0bdd95fd729c04388a
17 files changed
+673
-561
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -332,6 +332,10 @@ export function getChildHostContext() {
332
export const scheduleDeferredCallback = ReactScheduler.scheduleWork;
333
export const cancelDeferredCallback = ReactScheduler.cancelScheduledWork;
334
335
+export const scheduleTimeout = setTimeout;
336
+export const cancelTimeout = clearTimeout;
337
+export const noTimeout = -1;
338
+
339
export function shouldSetTextContent(type, props) {
340
return (
341
typeof props.children === 'string' || typeof props.children === 'number'
packages/react-dom/src/client/ReactDOMHostConfig.js
+6
@@ -45,6 +45,8 @@ type HostContextProd = string;
45
export type HostContext = HostContextDev | HostContextProd;
46
export type UpdatePayload = Array<mixed>;
47
export type ChildSet = void; // Unused
48
+export type TimeoutHandle = TimeoutID;
49
+export type NoTimeout = -1;
50
51
const {
52
createElement,
@@ -278,6 +280,10 @@ export const isPrimaryRenderer = true;
280
export const scheduleDeferredCallback = ReactScheduler.scheduleWork;
281
export const cancelDeferredCallback = ReactScheduler.cancelScheduledWork;
282
283
+export const scheduleTimeout = setTimeout;
284
+export const cancelTimeout = clearTimeout;
285
+export const noTimeout = -1;
286
+
287
// -------------------
288
// Mutation
289
// -------------------
packages/react-native-renderer/src/ReactFabricHostConfig.js
+7
@@ -66,6 +66,9 @@ export type HostContext = $ReadOnly<{|
66
|}>;
67
export type UpdatePayload = Object;
68
69
+export type TimeoutHandle = TimeoutID;
70
+export type NoTimeout = -1;
71
+
72
// TODO: Remove this conditional once all changes have propagated.
73
if (registerEventHandler) {
74
/**
@@ -324,6 +327,10 @@ export const scheduleDeferredCallback =
327
export const cancelDeferredCallback =
328
ReactNativeFrameScheduling.cancelDeferredCallback;
329
330
+export const scheduleTimeout = setTimeout;
331
+export const cancelTimeout = clearTimeout;
332
+export const noTimeout = -1;
333
+
334
// -------------------
335
// Persistence
336
// -------------------
packages/react-native-renderer/src/ReactNativeHostConfig.js
+7
@@ -42,6 +42,9 @@ export type HostContext = $ReadOnly<{|
42
export type UpdatePayload = Object; // Unused
43
export type ChildSet = void; // Unused
44
45
+export type TimeoutHandle = TimeoutID;
46
+export type NoTimeout = -1;
47
+
48
const UPDATE_SIGNAL = {};
49
if (__DEV__) {
50
Object.freeze(UPDATE_SIGNAL);
@@ -236,6 +239,10 @@ export const scheduleDeferredCallback =
239
export const cancelDeferredCallback =
240
ReactNativeFrameScheduling.cancelDeferredCallback;
241
242
+export const scheduleTimeout = setTimeout;
243
+export const cancelTimeout = clearTimeout;
244
+export const noTimeout = -1;
245
+
246
export function shouldDeprioritizeSubtree(type: string, props: Props): boolean {
247
return false;
248
}
packages/react-noop-renderer/src/createReactNoop.js
+4
@@ -182,6 +182,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
182
scheduledCallback = null;
183
},
184
185
+ scheduleTimeout: setTimeout,
186
+ cancelTimeout: clearTimeout,
187
+ noTimeout: -1,
188
+
189
prepareForCommit(): void {},
190
191
resetAfterCommit(): void {},
packages/react-reconciler/src/ReactFiberBeginWork.js
+15
-3
@@ -708,7 +708,7 @@ function updateTimeoutComponent(current, workInProgress, renderExpirationTime) {
708
const nextProps = workInProgress.pendingProps;
709
const prevProps = workInProgress.memoizedProps;
710
711
- const prevDidTimeout = workInProgress.memoizedState;
711
+ const prevDidTimeout = workInProgress.memoizedState === true;
712
713
// Check if we already attempted to render the normal state. If we did,
714
// and we timed out, render the placeholder state.
@@ -723,6 +723,16 @@ function updateTimeoutComponent(current, workInProgress, renderExpirationTime) {
723
return bailoutOnAlreadyFinishedWork(current, workInProgress);
724
}
725
726
+ if (nextDidTimeout) {
727
+ // If the timed-out view commits, schedule an update effect to record
728
+ // the committed time.
729
+ workInProgress.effectTag |= Update;
730
+ } else {
731
+ // The state node points to the time at which placeholder timed out.
732
+ // We can clear it once we switch back to the normal children.
733
+ workInProgress.stateNode = null;
734
+ }
735
+
736
const render = nextProps.children;
737
const nextChildren = render(nextDidTimeout);
738
workInProgress.memoizedProps = nextProps;
@@ -785,8 +795,9 @@ function propagateContextChange<V>(
795
if (fiber.type === context && (observedBits & changedBits) !== 0) {
796
// Update the expiration time of all the ancestors, including
797
// the alternates.
798
+
799
let node = fiber;
789
- while (node !== null) {
800
+ do {
801
const alternate = node.alternate;
802
if (
803
node.expirationTime === NoWork ||
@@ -812,7 +823,8 @@ function propagateContextChange<V>(
823
break;
824
}
825
node = node.return;
815
- }
826
+ } while (node !== null);
827
+
828
// Don't scan deeper than a matching consumer. When we render the
829
// consumer, we'll continue scanning from that point. This way the
830
// scanning work is time-sliced.
packages/react-reconciler/src/ReactFiberCommitWork.js
+9
-3
@@ -19,7 +19,7 @@ import type {FiberRoot} from './ReactFiberRoot';
19
import type {ExpirationTime} from './ReactFiberExpirationTime';
20
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
21
22
-import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
22
+import {enableProfilerTimer, enableSuspense} from 'shared/ReactFeatureFlags';
23
import {getCommitTime} from './ReactProfilerTimer';
24
import {
25
ClassComponent,
@@ -63,7 +63,10 @@ import {
63
replaceContainerChildren,
64
createContainerChildSet,
65
} from './ReactFiberHostConfig';
66
-import {captureCommitPhaseError} from './ReactFiberScheduler';
66
+import {
67
+ captureCommitPhaseError,
68
+ requestCurrentTime,
69
+} from './ReactFiberScheduler';
70
71
const {
72
invokeGuardedCallback,
@@ -314,7 +317,10 @@ function commitLifeCycles(
317
return;
318
}
319
case TimeoutComponent: {
317
- // We have no life-cycles associated with Timeouts.
320
+ if (enableSuspense) {
321
+ const currentTime = requestCurrentTime();
322
+ finishedWork.stateNode = {timedOutAt: currentTime};
323
+ }
324
return;
325
}
326
default: {
packages/react-reconciler/src/ReactFiberExpirationTime.js
+39
-4
@@ -32,10 +32,10 @@ function ceiling(num: number, precision: number): number {
32
return (((num / precision) | 0) + 1) * precision;
33
}
34
35
-export function computeExpirationBucket(
36
- currentTime: ExpirationTime,
37
- expirationInMs: number,
38
- bucketSizeMs: number,
35
+function computeExpirationBucket(
36
+ currentTime,
37
+ expirationInMs,
38
+ bucketSizeMs,
39
): ExpirationTime {
40
return (
41
MAGIC_NUMBER_OFFSET +
@@ -45,3 +45,38 @@ export function computeExpirationBucket(
45
)
46
);
47
}
48
+
49
+export const LOW_PRIORITY_EXPIRATION = 5000;
50
+export const LOW_PRIORITY_BATCH_SIZE = 250;
51
+
52
+export function computeAsyncExpiration(
53
+ currentTime: ExpirationTime,
54
+): ExpirationTime {
55
+ return computeExpirationBucket(
56
+ currentTime,
57
+ LOW_PRIORITY_EXPIRATION,
58
+ LOW_PRIORITY_BATCH_SIZE,
59
+ );
60
+}
61
+
62
+// We intentionally set a higher expiration time for interactive updates in
63
+// dev than in production.
64
+//
65
+// If the main thread is being blocked so long that you hit the expiration,
66
+// it's a problem that could be solved with better scheduling.
67
+//
68
+// People will be more likely to notice this and fix it with the long
69
+// expiration time in development.
70
+//
71
+// In production we opt for better UX at the risk of masking scheduling
72
+// problems, by expiring fast.
73
+export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
74
+export const HIGH_PRIORITY_BATCH_SIZE = 100;
75
+
76
+export function computeInteractiveExpiration(currentTime: ExpirationTime) {
77
+ return computeExpirationBucket(
78
+ currentTime,
79
+ HIGH_PRIORITY_EXPIRATION,
80
+ HIGH_PRIORITY_BATCH_SIZE,
81
+ );
82
+}
packages/react-reconciler/src/ReactFiberPendingPriority.js
+109
-56
@@ -10,9 +10,11 @@
10
import type {FiberRoot} from './ReactFiberRoot';
11
import type {ExpirationTime} from './ReactFiberExpirationTime';
12
13
-import {NoWork, Sync} from './ReactFiberExpirationTime';
13
+import {NoWork} from './ReactFiberExpirationTime';
14
15
-// TODO: Offscreen updates
15
+// TODO: Offscreen updates should never suspend. However, a promise that
16
+// suspended inside an offscreen subtree should be able to ping at the priority
17
+// of the outer render.
18
19
export function markPendingPriorityLevel(
20
root: FiberRoot,
@@ -40,7 +42,7 @@ export function markPendingPriorityLevel(
42
}
43
}
44
}
43
- findNextPendingPriorityLevel(root);
45
+ findNextExpirationTimeToWorkOn(expirationTime, root);
46
}
47
48
export function markCommittedPriorityLevels(
@@ -56,7 +58,7 @@ export function markCommittedPriorityLevels(
58
root.earliestSuspendedTime = NoWork;
59
root.latestSuspendedTime = NoWork;
60
root.latestPingedTime = NoWork;
59
- findNextPendingPriorityLevel(root);
61
+ findNextExpirationTimeToWorkOn(NoWork, root);
62
return;
63
}
64
@@ -85,7 +87,7 @@ export function markCommittedPriorityLevels(
87
// There's no suspended work. Treat the earliest remaining level as a
88
// pending level.
89
markPendingPriorityLevel(root, earliestRemainingTime);
88
- findNextPendingPriorityLevel(root);
90
+ findNextExpirationTimeToWorkOn(NoWork, root);
91
return;
92
}
93
@@ -100,7 +102,7 @@ export function markCommittedPriorityLevels(
102
// There's no suspended work. Treat the earliest remaining level as a
103
// pending level.
104
markPendingPriorityLevel(root, earliestRemainingTime);
103
- findNextPendingPriorityLevel(root);
105
+ findNextExpirationTimeToWorkOn(NoWork, root);
106
return;
107
}
108
@@ -108,38 +110,50 @@ export function markCommittedPriorityLevels(
110
// The earliest remaining time is earlier than all the suspended work.
111
// Treat it as a pending update.
112
markPendingPriorityLevel(root, earliestRemainingTime);
111
- findNextPendingPriorityLevel(root);
113
+ findNextExpirationTimeToWorkOn(NoWork, root);
114
return;
115
}
116
117
// The earliest remaining time falls within the range of known suspended
118
// levels. We should treat this as suspended work.
117
- findNextPendingPriorityLevel(root);
119
+ findNextExpirationTimeToWorkOn(NoWork, root);
120
}
121
122
export function hasLowerPriorityWork(
123
root: FiberRoot,
122
- renderExpirationTime: ExpirationTime,
123
-) {
124
+ erroredExpirationTime: ExpirationTime,
125
+): boolean {
126
+ const latestPendingTime = root.latestPendingTime;
127
+ const latestSuspendedTime = root.latestSuspendedTime;
128
+ const latestPingedTime = root.latestPingedTime;
129
+ return (
130
+ (latestPendingTime !== NoWork &&
131
+ latestPendingTime > erroredExpirationTime) ||
132
+ (latestSuspendedTime !== NoWork &&
133
+ latestSuspendedTime > erroredExpirationTime) ||
134
+ (latestPingedTime !== NoWork && latestPingedTime > erroredExpirationTime)
135
+ );
136
+}
137
+
138
+export function isPriorityLevelSuspended(
139
+ root: FiberRoot,
140
+ expirationTime: ExpirationTime,
141
+): boolean {
142
+ const earliestSuspendedTime = root.earliestSuspendedTime;
143
+ const latestSuspendedTime = root.latestSuspendedTime;
144
return (
125
- renderExpirationTime !== root.latestPendingTime &&
126
- renderExpirationTime !== root.latestSuspendedTime
145
+ earliestSuspendedTime !== NoWork &&
146
+ expirationTime >= earliestSuspendedTime &&
147
+ expirationTime <= latestSuspendedTime
148
);
149
}
150
151
export function markSuspendedPriorityLevel(
152
root: FiberRoot,
153
suspendedTime: ExpirationTime,
133
- didError: boolean,
154
): void {
135
- if (didError && !hasLowerPriorityWork(root, suspendedTime)) {
136
- // TODO: When we add back resuming, we need to ensure the progressed work
137
- // is thrown out and not reused during the restarted render. One way to
138
- // invalidate the progressed work is to restart at expirationTime + 1.
139
- root.didError = true;
140
- findNextPendingPriorityLevel(root);
141
- return;
142
- }
155
+ root.didError = false;
156
+ clearPing(root, suspendedTime);
157
158
// First, check the known pending levels and update them if needed.
159
const earliestPendingTime = root.earliestPendingTime;
@@ -159,15 +173,9 @@ export function markSuspendedPriorityLevel(
173
root.latestPendingTime = earliestPendingTime;
174
}
175
162
- // Next, if we're working on the lowest known suspended level, clear the ping.
163
- // TODO: What if a promise suspends and pings before the root completes?
164
- const latestSuspendedTime = root.latestSuspendedTime;
165
- if (latestSuspendedTime === suspendedTime) {
166
- root.latestPingedTime = NoWork;
167
- }
168
-
176
// Finally, update the known suspended levels.
177
const earliestSuspendedTime = root.earliestSuspendedTime;
178
+ const latestSuspendedTime = root.latestSuspendedTime;
179
if (earliestSuspendedTime === NoWork) {
180
// No other suspended levels.
181
root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;
@@ -180,47 +188,92 @@ export function markSuspendedPriorityLevel(
188
root.latestSuspendedTime = suspendedTime;
189
}
190
}
183
- findNextPendingPriorityLevel(root);
191
+
192
+ findNextExpirationTimeToWorkOn(suspendedTime, root);
193
}
194
195
export function markPingedPriorityLevel(
196
root: FiberRoot,
197
pingedTime: ExpirationTime,
198
): void {
190
- const latestSuspendedTime = root.latestSuspendedTime;
191
- if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) {
192
- const latestPingedTime = root.latestPingedTime;
193
- if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
194
- root.latestPingedTime = pingedTime;
195
- }
199
+ root.didError = false;
200
+
201
+ // TODO: When we add back resuming, we need to ensure the progressed work
202
+ // is thrown out and not reused during the restarted render. One way to
203
+ // invalidate the progressed work is to restart at expirationTime + 1.
204
+ const latestPingedTime = root.latestPingedTime;
205
+ if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
206
+ root.latestPingedTime = pingedTime;
207
}
197
- findNextPendingPriorityLevel(root);
208
+ findNextExpirationTimeToWorkOn(pingedTime, root);
209
}
210
200
-function findNextPendingPriorityLevel(root) {
211
+function clearPing(root, completedTime) {
212
+ // TODO: Track whether the root was pinged during the render phase. If so,
213
+ // we need to make sure we don't lose track of it.
214
+ const latestPingedTime = root.latestPingedTime;
215
+ if (latestPingedTime !== NoWork && latestPingedTime <= completedTime) {
216
+ root.latestPingedTime = NoWork;
217
+ }
218
+}
219
+
220
+export function findEarliestOutstandingPriorityLevel(
221
+ root: FiberRoot,
222
+ renderExpirationTime: ExpirationTime,
223
+): ExpirationTime {
224
+ let earliestExpirationTime = renderExpirationTime;
225
+
226
+ const earliestPendingTime = root.earliestPendingTime;
227
const earliestSuspendedTime = root.earliestSuspendedTime;
228
+ if (
229
+ earliestExpirationTime === NoWork ||
230
+ (earliestPendingTime !== NoWork &&
231
+ earliestPendingTime < earliestExpirationTime)
232
+ ) {
233
+ earliestExpirationTime = earliestPendingTime;
234
+ }
235
+ if (
236
+ earliestExpirationTime === NoWork ||
237
+ (earliestSuspendedTime !== NoWork &&
238
+ earliestSuspendedTime < earliestExpirationTime)
239
+ ) {
240
+ earliestExpirationTime = earliestSuspendedTime;
241
+ }
242
+ return earliestExpirationTime;
243
+}
244
+
245
+function findNextExpirationTimeToWorkOn(completedExpirationTime, root) {
246
+ const earliestSuspendedTime = root.earliestSuspendedTime;
247
+ const latestSuspendedTime = root.latestSuspendedTime;
248
const earliestPendingTime = root.earliestPendingTime;
203
- let nextExpirationTimeToWorkOn;
204
- let expirationTime;
205
- if (earliestSuspendedTime === NoWork) {
206
- // Fast path. There's no suspended work.
207
- nextExpirationTimeToWorkOn = expirationTime = earliestPendingTime;
208
- } else if (earliestPendingTime !== NoWork) {
209
- // Check if there's known pending work.
210
- nextExpirationTimeToWorkOn = earliestPendingTime;
211
- expirationTime =
212
- earliestSuspendedTime < earliestPendingTime
213
- ? earliestSuspendedTime
214
- : earliestPendingTime;
215
- } else {
216
- // Finally, if a suspended level was pinged, work on that. Otherwise there's
217
- // nothing to work on.
218
- nextExpirationTimeToWorkOn = expirationTime = root.latestPingedTime;
249
+ const latestPingedTime = root.latestPingedTime;
250
+
251
+ // Work on the earliest pending time. Failing that, work on the latest
252
+ // pinged time.
253
+ let nextExpirationTimeToWorkOn =
254
+ earliestPendingTime !== NoWork ? earliestPendingTime : latestPingedTime;
255
+
256
+ // If there is no pending or pinted work, check if there's suspended work
257
+ // that's lower priority than what we just completed.
258
+ if (
259
+ nextExpirationTimeToWorkOn === NoWork &&
260
+ (completedExpirationTime === NoWork ||
261
+ latestSuspendedTime > completedExpirationTime)
262
+ ) {
263
+ // The lowest priority suspended work is the work most likely to be
264
+ // committed next. Let's start rendering it again, so that if it times out,
265
+ // it's ready to commit.
266
+ nextExpirationTimeToWorkOn = latestSuspendedTime;
267
}
268
221
- if (root.didError) {
222
- // Revert to synchronous mode.
223
- expirationTime = Sync;
269
+ let expirationTime = nextExpirationTimeToWorkOn;
270
+ if (
271
+ expirationTime !== NoWork &&
272
+ earliestSuspendedTime !== NoWork &&
273
+ earliestSuspendedTime < expirationTime
274
+ ) {
275
+ // Expire using the earliest known expiration time.
276
+ expirationTime = earliestSuspendedTime;
277
}
278
279
root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn;
packages/react-reconciler/src/ReactFiberRoot.js
+7
-3
@@ -9,6 +9,9 @@
9
10
import type {Fiber} from './ReactFiber';
11
import type {ExpirationTime} from './ReactFiberExpirationTime';
12
+import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
13
+
14
+import {noTimeout} from './ReactFiberHostConfig';
15
16
import {createHostRootFiber} from './ReactFiber';
17
import {NoWork} from './ReactFiberExpirationTime';
@@ -51,10 +54,10 @@ export type FiberRoot = {
54
55
pendingCommitExpirationTime: ExpirationTime,
56
// A finished work-in-progress HostRoot that's ready to be committed.
54
- // TODO: The reason this is separate from isReadyForCommit is because the
55
- // FiberRoot concept will likely be lifted out of the reconciler and into
56
- // the renderer.
57
finishedWork: Fiber | null,
58
+ // Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
59
+ // it's superseded by a new one.
60
+ timeoutHandle: TimeoutHandle | NoTimeout,
61
// Top context object, used by renderSubtreeIntoContainer
62
context: Object | null,
63
pendingContext: Object | null,
@@ -95,6 +98,7 @@ export function createFiberRoot(
98
99
pendingCommitExpirationTime: NoWork,
100
finishedWork: null,
101
+ timeoutHandle: noTimeout,
102
context: null,
103
pendingContext: null,
104
hydrate,
packages/react-reconciler/src/ReactFiberScheduler.js
+291
-163
@@ -43,11 +43,18 @@ import {
43
replayFailedUnitOfWorkWithInvokeGuardedCallback,
44
warnAboutDeprecatedLifecycles,
45
warnAboutLegacyContextAPI,
46
+ enableSuspense,
47
} from 'shared/ReactFeatureFlags';
48
import getComponentName from 'shared/getComponentName';
49
import invariant from 'shared/invariant';
50
import warning from 'shared/warning';
51
52
+import {
53
+ scheduleTimeout,
54
+ cancelTimeout,
55
+ noTimeout,
56
+} from './ReactFiberHostConfig';
57
+
58
import ReactFiberInstrumentation from './ReactFiberInstrumentation';
59
import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
60
import {
@@ -62,6 +69,9 @@ import {
69
markCommittedPriorityLevels,
70
markSuspendedPriorityLevel,
71
markPingedPriorityLevel,
72
+ hasLowerPriorityWork,
73
+ isPriorityLevelSuspended,
74
+ findEarliestOutstandingPriorityLevel,
75
} from './ReactFiberPendingPriority';
76
import {
77
recordEffect,
@@ -90,7 +100,8 @@ import {
100
Never,
101
msToExpirationTime,
102
expirationTimeToMs,
93
- computeExpirationBucket,
103
+ computeAsyncExpiration,
104
+ computeInteractiveExpiration,
105
} from './ReactFiberExpirationTime';
106
import {AsyncMode, ProfileMode} from './ReactTypeOfMode';
107
import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
@@ -223,7 +234,7 @@ let nextUnitOfWork: Fiber | null = null;
234
let nextRoot: FiberRoot | null = null;
235
// The time at which we're currently rendering work.
236
let nextRenderExpirationTime: ExpirationTime = NoWork;
226
-let nextLatestTimeoutMs: number = -1;
237
+let nextLatestAbsoluteTimeoutMs: number = -1;
238
let nextRenderDidError: boolean = false;
239
240
// The next fiber with an effect that we're currently committing.
@@ -338,7 +349,7 @@ function resetStack() {
349
350
nextRoot = null;
351
nextRenderExpirationTime = NoWork;
341
- nextLatestTimeoutMs = -1;
352
+ nextLatestAbsoluteTimeoutMs = -1;
353
nextRenderDidError = false;
354
nextUnitOfWork = null;
355
}
@@ -978,7 +989,11 @@ function workLoop(isYieldy) {
989
}
990
}
991
981
-function renderRoot(root: FiberRoot, isYieldy: boolean): void {
992
+function renderRoot(
993
+ root: FiberRoot,
994
+ isYieldy: boolean,
995
+ isExpired: boolean,
996
+): void {
997
invariant(
998
!isWorking,
999
'renderRoot was called recursively. This error is likely caused ' +
@@ -1086,44 +1101,10 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1101
// there's no more work to be done in the current batch.
1102
nextRoot = null;
1103
onFatal(root);
1089
- } else if (nextUnitOfWork === null) {
1090
- // We reached the root.
1091
- const rootWorkInProgress = root.current.alternate;
1092
- invariant(
1093
- rootWorkInProgress !== null,
1094
- 'Finished root should have a work-in-progress. This error is likely ' +
1095
- 'caused by a bug in React. Please file an issue.',
1096
- );
1097
- if ((rootWorkInProgress.effectTag & Incomplete) === NoEffect) {
1098
- // The root successfully completed.
1099
- const didCompleteRoot = true;
1100
- stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1101
- interruptedBy = null;
1102
- // `nextRoot` points to the in-progress root. A non-null value indicates
1103
- // that we're in the middle of an async render. Set it to null to indicate
1104
- // there's no more work to be done in the current batch.
1105
- nextRoot = null;
1106
- onComplete(root, rootWorkInProgress, expirationTime);
1107
- } else {
1108
- // The root did not complete.
1109
- const didCompleteRoot = false;
1110
- stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1111
- interruptedBy = null;
1112
- // `nextRoot` points to the in-progress root. A non-null value indicates
1113
- // that we're in the middle of an async render. Set it to null to indicate
1114
- // there's no more work to be done in the current batch.
1115
- nextRoot = null;
1116
- markSuspendedPriorityLevel(root, expirationTime, nextRenderDidError);
1117
- const suspendedExpirationTime = expirationTime;
1118
- const newExpirationTime = root.expirationTime;
1119
- onSuspend(
1120
- root,
1121
- suspendedExpirationTime,
1122
- newExpirationTime,
1123
- nextLatestTimeoutMs,
1124
- );
1125
- }
1126
- } else {
1104
+ return;
1105
+ }
1106
+
1107
+ if (nextUnitOfWork !== null) {
1108
// There's still remaining async work in this tree, but we ran out of time
1109
// in the current frame. Yield back to the renderer. Unless we're
1110
// interrupted by a higher priority update, we'll continue later from where
@@ -1132,7 +1113,103 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1113
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1114
interruptedBy = null;
1115
onYield(root);
1116
+ return;
1117
+ }
1118
+
1119
+ // We completed the whole tree.
1120
+ const didCompleteRoot = true;
1121
+ stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1122
+ const rootWorkInProgress = root.current.alternate;
1123
+ invariant(
1124
+ rootWorkInProgress !== null,
1125
+ 'Finished root should have a work-in-progress. This error is likely ' +
1126
+ 'caused by a bug in React. Please file an issue.',
1127
+ );
1128
+
1129
+ // `nextRoot` points to the in-progress root. A non-null value indicates
1130
+ // that we're in the middle of an async render. Set it to null to indicate
1131
+ // there's no more work to be done in the current batch.
1132
+ nextRoot = null;
1133
+ interruptedBy = null;
1134
+
1135
+ if (nextRenderDidError) {
1136
+ // There was an error
1137
+ if (hasLowerPriorityWork(root, expirationTime)) {
1138
+ // There's lower priority work. If so, it may have the effect of fixing
1139
+ // the exception that was just thrown. Exit without committing. This is
1140
+ // similar to a suspend, but without a timeout because we're not waiting
1141
+ // for a promise to resolve. React will restart at the lower
1142
+ // priority level.
1143
+ markSuspendedPriorityLevel(root, expirationTime);
1144
+ const suspendedExpirationTime = expirationTime;
1145
+ const rootExpirationTime = root.expirationTime;
1146
+ onSuspend(
1147
+ root,
1148
+ rootWorkInProgress,
1149
+ suspendedExpirationTime,
1150
+ rootExpirationTime,
1151
+ -1, // Indicates no timeout
1152
+ );
1153
+ return;
1154
+ } else if (!root.didError && expirationTime !== Sync) {
1155
+ // There's no lower priority work, but we're rendering asynchronously.
1156
+ // Synchronsouly attempt to render the same level one more time. This is
1157
+ // similar to a suspend, but without a timeout because we're not waiting
1158
+ // for a promise to resolve.
1159
+ root.didError = true;
1160
+ const suspendedExpirationTime = (root.nextExpirationTimeToWorkOn = expirationTime);
1161
+ const rootExpirationTime = (root.expirationTime = Sync);
1162
+ onSuspend(
1163
+ root,
1164
+ rootWorkInProgress,
1165
+ suspendedExpirationTime,
1166
+ rootExpirationTime,
1167
+ -1, // Indicates no timeout
1168
+ );
1169
+ return;
1170
+ }
1171
}
1172
+
1173
+ if (enableSuspense && !isExpired && nextLatestAbsoluteTimeoutMs !== -1) {
1174
+ // The tree was suspended.
1175
+ const suspendedExpirationTime = expirationTime;
1176
+ markSuspendedPriorityLevel(root, suspendedExpirationTime);
1177
+
1178
+ // Find the earliest uncommitted expiration time in the tree, including
1179
+ // work that is suspended. The timeout threshold cannot be longer than
1180
+ // the overall expiration.
1181
+ const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
1182
+ root,
1183
+ expirationTime,
1184
+ );
1185
+ const earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime);
1186
+ if (earliestExpirationTimeMs < nextLatestAbsoluteTimeoutMs) {
1187
+ nextLatestAbsoluteTimeoutMs = earliestExpirationTimeMs;
1188
+ }
1189
+
1190
+ // Subtract the current time from the absolute timeout to get the number
1191
+ // of milliseconds until the timeout. In other words, convert an absolute
1192
+ // timestamp to a relative time. This is the value that is passed
1193
+ // to `setTimeout`.
1194
+ const currentTimeMs = expirationTimeToMs(requestCurrentTime());
1195
+ let msUntilTimeout = nextLatestAbsoluteTimeoutMs - currentTimeMs;
1196
+ msUntilTimeout = msUntilTimeout < 0 ? 0 : msUntilTimeout;
1197
+
1198
+ // TODO: Account for the Just Noticable Difference
1199
+
1200
+ const rootExpirationTime = root.expirationTime;
1201
+ onSuspend(
1202
+ root,
1203
+ rootWorkInProgress,
1204
+ suspendedExpirationTime,
1205
+ rootExpirationTime,
1206
+ msUntilTimeout,
1207
+ );
1208
+ return;
1209
+ }
1210
+
1211
+ // Ready to commit.
1212
+ onComplete(root, rootWorkInProgress, expirationTime);
1213
}
1214
1215
function dispatch(
@@ -1193,36 +1270,6 @@ function captureCommitPhaseError(fiber: Fiber, error: mixed) {
1270
return dispatch(fiber, error, Sync);
1271
}
1272
1196
-function computeAsyncExpiration(currentTime) {
1197
- // Given the current clock time, returns an expiration time. We use rounding
1198
- // to batch like updates together.
1199
- // Should complete within ~5000ms. 5250ms max.
1200
- const expirationMs = 5000;
1201
- const bucketSizeMs = 250;
1202
- return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
1203
-}
1204
-
1205
-function computeInteractiveExpiration(currentTime) {
1206
- let expirationMs;
1207
- // We intentionally set a higher expiration time for interactive updates in
1208
- // dev than in production.
1209
- // If the main thread is being blocked so long that you hit the expiration,
1210
- // it's a problem that could be solved with better scheduling.
1211
- // People will be more likely to notice this and fix it with the long
1212
- // expiration time in development.
1213
- // In production we opt for better UX at the risk of masking scheduling
1214
- // problems, by expiring fast.
1215
- if (__DEV__) {
1216
- // Should complete within ~500ms. 600ms max.
1217
- expirationMs = 500;
1218
- } else {
1219
- // In production things should be more responsive, 150ms max.
1220
- expirationMs = 150;
1221
- }
1222
- const bucketSizeMs = 100;
1223
- return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
1224
-}
1225
-
1273
// Creates a unique async expiration time.
1274
function computeUniqueAsyncExpiration(): ExpirationTime {
1275
const currentTime = requestCurrentTime();
@@ -1287,101 +1334,131 @@ function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
1334
return expirationTime;
1335
}
1336
1290
-function markTimeout(
1337
+function renderDidSuspend(
1338
root: FiberRoot,
1292
- thenable: Thenable,
1293
- timeoutMs: number,
1339
+ absoluteTimeoutMs: number,
1340
suspendedTime: ExpirationTime,
1341
) {
1342
// Schedule the timeout.
1297
- if (timeoutMs >= 0 && nextLatestTimeoutMs < timeoutMs) {
1298
- nextLatestTimeoutMs = timeoutMs;
1343
+ if (
1344
+ absoluteTimeoutMs >= 0 &&
1345
+ nextLatestAbsoluteTimeoutMs < absoluteTimeoutMs
1346
+ ) {
1347
+ nextLatestAbsoluteTimeoutMs = absoluteTimeoutMs;
1348
}
1349
}
1350
1302
-function markError(root: FiberRoot) {
1351
+function renderDidError(root: FiberRoot) {
1352
nextRenderDidError = true;
1353
}
1354
1306
-function retrySuspendedRoot(root: FiberRoot, suspendedTime: ExpirationTime) {
1307
- markPingedPriorityLevel(root, suspendedTime);
1308
- const retryTime = root.expirationTime;
1309
- if (retryTime !== NoWork) {
1310
- requestWork(root, retryTime);
1311
- }
1312
-}
1355
+function retrySuspendedRoot(
1356
+ root: FiberRoot,
1357
+ fiber: Fiber,
1358
+ suspendedTime: ExpirationTime,
1359
+) {
1360
+ if (enableSuspense) {
1361
+ let retryTime;
1362
1314
-function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1315
- recordScheduleUpdate();
1363
+ if (isPriorityLevelSuspended(root, suspendedTime)) {
1364
+ // Ping at the original level
1365
+ retryTime = suspendedTime;
1366
+ markPingedPriorityLevel(root, retryTime);
1367
+ } else {
1368
+ // Placeholder already timed out. Compute a new expiration time
1369
+ const currentTime = requestCurrentTime();
1370
+ retryTime = computeExpirationForFiber(currentTime, fiber);
1371
+ markPendingPriorityLevel(root, retryTime);
1372
+ }
1373
1317
- if (__DEV__) {
1318
- if (fiber.tag === ClassComponent) {
1319
- const instance = fiber.stateNode;
1320
- warnAboutInvalidUpdates(instance);
1374
+ scheduleWorkToRoot(fiber, retryTime);
1375
+ const rootExpirationTime = root.expirationTime;
1376
+ if (rootExpirationTime !== NoWork) {
1377
+ requestWork(root, rootExpirationTime);
1378
}
1379
}
1380
+}
1381
1382
+function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
1383
+ // Walk the parent path to the root and update each node's
1384
+ // expiration time.
1385
let node = fiber;
1325
- while (node !== null) {
1326
- // Walk the parent path to the root and update each node's
1327
- // expiration time.
1386
+ do {
1387
+ const alternate = node.alternate;
1388
if (
1389
node.expirationTime === NoWork ||
1390
node.expirationTime > expirationTime
1391
) {
1392
node.expirationTime = expirationTime;
1333
- }
1334
- if (node.alternate !== null) {
1393
if (
1336
- node.alternate.expirationTime === NoWork ||
1337
- node.alternate.expirationTime > expirationTime
1394
+ alternate !== null &&
1395
+ (alternate.expirationTime === NoWork ||
1396
+ alternate.expirationTime > expirationTime)
1397
) {
1339
- node.alternate.expirationTime = expirationTime;
1398
+ alternate.expirationTime = expirationTime;
1399
}
1400
+ } else if (
1401
+ alternate !== null &&
1402
+ (alternate.expirationTime === NoWork ||
1403
+ alternate.expirationTime > expirationTime)
1404
+ ) {
1405
+ alternate.expirationTime = expirationTime;
1406
}
1342
- if (node.return === null) {
1343
- if (node.tag === HostRoot) {
1344
- const root: FiberRoot = (node.stateNode: any);
1345
- if (
1346
- !isWorking &&
1347
- nextRenderExpirationTime !== NoWork &&
1348
- expirationTime < nextRenderExpirationTime
1349
- ) {
1350
- // This is an interruption. (Used for performance tracking.)
1351
- interruptedBy = fiber;
1352
- resetStack();
1353
- }
1354
- markPendingPriorityLevel(root, expirationTime);
1355
- if (
1356
- // If we're in the render phase, we don't need to schedule this root
1357
- // for an update, because we'll do it before we exit...
1358
- !isWorking ||
1359
- isCommitting ||
1360
- // ...unless this is a different root than the one we're rendering.
1361
- nextRoot !== root
1362
- ) {
1363
- const rootExpirationTime = root.expirationTime;
1364
- requestWork(root, rootExpirationTime);
1365
- }
1366
- if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1367
- invariant(
1368
- false,
1369
- 'Maximum update depth exceeded. This can happen when a ' +
1370
- 'component repeatedly calls setState inside ' +
1371
- 'componentWillUpdate or componentDidUpdate. React limits ' +
1372
- 'the number of nested updates to prevent infinite loops.',
1373
- );
1374
- }
1375
- } else {
1376
- if (__DEV__) {
1377
- if (fiber.tag === ClassComponent) {
1378
- warnAboutUpdateOnUnmounted(fiber);
1379
- }
1380
- }
1381
- return;
1382
- }
1407
+ if (node.return === null && node.tag === HostRoot) {
1408
+ return node.stateNode;
1409
}
1410
node = node.return;
1411
+ } while (node !== null);
1412
+ return null;
1413
+}
1414
+
1415
+function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1416
+ recordScheduleUpdate();
1417
+
1418
+ if (__DEV__) {
1419
+ if (fiber.tag === ClassComponent) {
1420
+ const instance = fiber.stateNode;
1421
+ warnAboutInvalidUpdates(instance);
1422
+ }
1423
+ }
1424
+
1425
+ const root = scheduleWorkToRoot(fiber, expirationTime);
1426
+ if (root === null) {
1427
+ if (__DEV__ && fiber.tag === ClassComponent) {
1428
+ warnAboutUpdateOnUnmounted(fiber);
1429
+ }
1430
+ return;
1431
+ }
1432
+
1433
+ if (
1434
+ !isWorking &&
1435
+ nextRenderExpirationTime !== NoWork &&
1436
+ expirationTime < nextRenderExpirationTime
1437
+ ) {
1438
+ // This is an interruption. (Used for performance tracking.)
1439
+ interruptedBy = fiber;
1440
+ resetStack();
1441
+ }
1442
+ markPendingPriorityLevel(root, expirationTime);
1443
+ if (
1444
+ // If we're in the render phase, we don't need to schedule this root
1445
+ // for an update, because we'll do it before we exit...
1446
+ !isWorking ||
1447
+ isCommitting ||
1448
+ // ...unless this is a different root than the one we're rendering.
1449
+ nextRoot !== root
1450
+ ) {
1451
+ const rootExpirationTime = root.expirationTime;
1452
+ requestWork(root, rootExpirationTime);
1453
+ }
1454
+ if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1455
+ invariant(
1456
+ false,
1457
+ 'Maximum update depth exceeded. This can happen when a ' +
1458
+ 'component repeatedly calls setState inside ' +
1459
+ 'componentWillUpdate or componentDidUpdate. React limits ' +
1460
+ 'the number of nested updates to prevent infinite loops.',
1461
+ );
1462
}
1463
}
1464
@@ -1497,23 +1574,43 @@ function onComplete(
1574
1575
function onSuspend(
1576
root: FiberRoot,
1577
+ finishedWork: Fiber,
1578
suspendedExpirationTime: ExpirationTime,
1501
- expirationTime: ExpirationTime,
1502
- timeoutMs: number,
1503
-) {
1504
- if (timeoutMs >= 0) {
1505
- const suspendedRoot = root;
1506
- setTimeout(() => {
1507
- retrySuspendedRoot(suspendedRoot, suspendedExpirationTime);
1508
- }, timeoutMs);
1579
+ rootExpirationTime: ExpirationTime,
1580
+ msUntilTimeout: number,
1581
+): void {
1582
+ root.expirationTime = rootExpirationTime;
1583
+ if (enableSuspense && msUntilTimeout === 0 && !shouldYield()) {
1584
+ // Don't wait an additional tick. Commit the tree immediately.
1585
+ root.pendingCommitExpirationTime = suspendedExpirationTime;
1586
+ root.finishedWork = finishedWork;
1587
+ } else if (msUntilTimeout > 0) {
1588
+ // Wait `msUntilTimeout` milliseconds before committing.
1589
+ root.timeoutHandle = scheduleTimeout(
1590
+ onTimeout.bind(null, root, finishedWork, suspendedExpirationTime),
1591
+ msUntilTimeout,
1592
+ );
1593
}
1510
- root.expirationTime = expirationTime;
1594
}
1595
1596
function onYield(root) {
1597
root.finishedWork = null;
1598
}
1599
1600
+function onTimeout(root, finishedWork, suspendedExpirationTime) {
1601
+ if (enableSuspense) {
1602
+ // The root timed out. Commit it.
1603
+ root.pendingCommitExpirationTime = suspendedExpirationTime;
1604
+ root.finishedWork = finishedWork;
1605
+ // Read the current time before entering the commit phase. We can be
1606
+ // certain this won't cause tearing related to batching of event updates
1607
+ // because we're at the top of a timer event.
1608
+ recomputeCurrentRendererTime();
1609
+ currentSchedulerTime = currentRendererTime;
1610
+ flushRoot(root, suspendedExpirationTime);
1611
+ }
1612
+}
1613
+
1614
function onCommit(root, expirationTime) {
1615
root.expirationTime = expirationTime;
1616
root.finishedWork = null;
@@ -1567,7 +1664,6 @@ function requestCurrentTime() {
1664
// It's up to the renderer to call renderRoot at some point in the future.
1665
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
1666
addRootToSchedule(root, expirationTime);
1570
-
1667
if (isRendering) {
1668
// Prevent reentrancy. Remaining work will be scheduled at the end of
1669
// the currently rendering batch.
@@ -1581,7 +1677,7 @@ function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
1677
// flush it now.
1678
nextFlushedRoot = root;
1679
nextFlushedExpirationTime = Sync;
1584
- performWorkOnRoot(root, Sync, false);
1680
+ performWorkOnRoot(root, Sync, true);
1681
}
1682
return;
1683
}
@@ -1733,7 +1829,11 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1829
minExpirationTime >= nextFlushedExpirationTime) &&
1830
(!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
1831
) {
1736
- performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
1832
+ performWorkOnRoot(
1833
+ nextFlushedRoot,
1834
+ nextFlushedExpirationTime,
1835
+ currentRendererTime >= nextFlushedExpirationTime,
1836
+ );
1837
findHighestPriorityRoot();
1838
recomputeCurrentRendererTime();
1839
currentSchedulerTime = currentRendererTime;
@@ -1745,7 +1845,7 @@ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1845
(minExpirationTime === NoWork ||
1846
minExpirationTime >= nextFlushedExpirationTime)
1847
) {
1748
- performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
1848
+ performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
1849
findHighestPriorityRoot();
1850
}
1851
}
@@ -1781,7 +1881,7 @@ function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1881
// including the given time.
1882
nextFlushedRoot = root;
1883
nextFlushedExpirationTime = expirationTime;
1784
- performWorkOnRoot(root, expirationTime, false);
1884
+ performWorkOnRoot(root, expirationTime, true);
1885
// Flush any sync work that was scheduled by lifecycles
1886
performSyncWork();
1887
finishRendering();
@@ -1817,7 +1917,7 @@ function finishRendering() {
1917
function performWorkOnRoot(
1918
root: FiberRoot,
1919
expirationTime: ExpirationTime,
1820
- isYieldy: boolean,
1920
+ isExpired: boolean,
1921
) {
1922
invariant(
1923
!isRendering,
@@ -1828,14 +1928,28 @@ function performWorkOnRoot(
1928
isRendering = true;
1929
1930
// Check if this is async work or sync/expired work.
1831
- if (!isYieldy) {
1931
+ if (deadline === null || isExpired) {
1932
// Flush work without yielding.
1933
+ // TODO: Non-yieldy work does not necessarily imply expired work. A renderer
1934
+ // may want to perform some work without yielding, but also without
1935
+ // requiring the root to complete (by triggering placeholders).
1936
+
1937
let finishedWork = root.finishedWork;
1938
if (finishedWork !== null) {
1939
// This root is already complete. We can commit it.
1940
completeRoot(root, finishedWork, expirationTime);
1941
} else {
1838
- renderRoot(root, false);
1942
+ root.finishedWork = null;
1943
+ // If this root previously suspended, clear its existing timeout, since
1944
+ // we're about to try rendering again.
1945
+ const timeoutHandle = root.timeoutHandle;
1946
+ if (enableSuspense && timeoutHandle !== noTimeout) {
1947
+ root.timeoutHandle = noTimeout;
1948
+ // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
1949
+ cancelTimeout(timeoutHandle);
1950
+ }
1951
+ const isYieldy = false;
1952
+ renderRoot(root, isYieldy, isExpired);
1953
finishedWork = root.finishedWork;
1954
if (finishedWork !== null) {
1955
// We've completed the root. Commit it.
@@ -1849,7 +1963,17 @@ function performWorkOnRoot(
1963
// This root is already complete. We can commit it.
1964
completeRoot(root, finishedWork, expirationTime);
1965
} else {
1852
- renderRoot(root, true);
1966
+ root.finishedWork = null;
1967
+ // If this root previously suspended, clear its existing timeout, since
1968
+ // we're about to try rendering again.
1969
+ const timeoutHandle = root.timeoutHandle;
1970
+ if (enableSuspense && timeoutHandle !== noTimeout) {
1971
+ root.timeoutHandle = noTimeout;
1972
+ // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
1973
+ cancelTimeout(timeoutHandle);
1974
+ }
1975
+ const isYieldy = true;
1976
+ renderRoot(root, isYieldy, isExpired);
1977
finishedWork = root.finishedWork;
1978
if (finishedWork !== null) {
1979
// We've completed the root. Check the deadline one more time
@@ -1899,16 +2023,20 @@ function completeRoot(
2023
2024
// Commit the root.
2025
root.finishedWork = null;
2026
+
2027
commitRoot(root, finishedWork);
2028
}
2029
2030
// When working on async work, the reconciler asks the renderer if it should
2031
// yield execution. For DOM, we implement this with requestIdleCallback.
2032
function shouldYield() {
1908
- if (deadline === null || deadlineDidExpire) {
1909
- return false;
2033
+ if (deadlineDidExpire) {
2034
+ return true;
2035
}
1911
- if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
2036
+ if (
2037
+ deadline === null ||
2038
+ deadline.timeRemaining() > timeHeuristicForUnitOfWork
2039
+ ) {
2040
// Disregard deadline.didTimeout. Only expired work should be flushed
2041
// during a timeout. This path is only hit for non-expired work.
2042
return false;
@@ -2037,8 +2165,8 @@ export {
2165
computeExpirationForFiber,
2166
captureCommitPhaseError,
2167
onUncaughtError,
2040
- markTimeout,
2041
- markError,
2168
+ renderDidSuspend,
2169
+ renderDidError,
2170
retrySuspendedRoot,
2171
markLegacyErrorBoundaryAsFailed,
2172
isAlreadyFailedLegacyErrorBoundary,
packages/react-reconciler/src/ReactFiberUnwindWork.js
+98
-104
@@ -39,11 +39,9 @@ import {createCapturedValue} from './ReactCapturedValue';
39
import {
40
enqueueCapturedUpdate,
41
createUpdate,
42
- enqueueUpdate,
42
CaptureUpdate,
43
} from './ReactUpdateQueue';
44
import {logError} from './ReactFiberCommitWork';
46
-import {Never, Sync, expirationTimeToMs} from './ReactFiberExpirationTime';
45
import {popHostContainer, popHostContext} from './ReactFiberHostContext';
46
import {
47
popContextProvider as popLegacyContextProvider,
@@ -55,23 +53,27 @@ import {
53
recordElapsedActualRenderTime,
54
} from './ReactProfilerTimer';
55
import {
58
- markTimeout,
59
- markError,
56
+ renderDidSuspend,
57
+ renderDidError,
58
onUncaughtError,
59
markLegacyErrorBoundaryAsFailed,
60
isAlreadyFailedLegacyErrorBoundary,
63
- requestCurrentTime,
64
- computeExpirationForFiber,
65
- scheduleWork,
61
retrySuspendedRoot,
62
} from './ReactFiberScheduler';
68
-import {hasLowerPriorityWork} from './ReactFiberPendingPriority';
63
+
64
+import invariant from 'shared/invariant';
65
+import maxSigned31BitInt from './maxSigned31BitInt';
66
+import {
67
+ expirationTimeToMs,
68
+ LOW_PRIORITY_EXPIRATION,
69
+} from './ReactFiberExpirationTime';
70
+import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
71
72
function createRootErrorUpdate(
73
fiber: Fiber,
74
errorInfo: CapturedValue<mixed>,
75
expirationTime: ExpirationTime,
74
-): Update<null> {
76
+): Update<mixed> {
77
const update = createUpdate(expirationTime);
78
// Unmount the root by rendering null.
79
update.tag = CaptureUpdate;
@@ -129,16 +131,6 @@ function createClassErrorUpdate(
131
return update;
132
}
133
132
-function schedulePing(finishedWork) {
133
- // Once the promise resolves, we should try rendering the non-
134
- // placeholder state again.
135
- const currentTime = requestCurrentTime();
136
- const expirationTime = computeExpirationForFiber(currentTime, finishedWork);
137
- const recoveryUpdate = createUpdate(expirationTime);
138
- enqueueUpdate(finishedWork, recoveryUpdate, expirationTime);
139
- scheduleWork(finishedWork, expirationTime);
140
-}
141
-
134
function throwException(
135
root: FiberRoot,
136
returnFiber: Fiber,
@@ -160,113 +152,113 @@ function throwException(
152
// This is a thenable.
153
const thenable: Thenable = (value: any);
154
163
- // TODO: Should use the earliest known expiration time
164
- const currentTime = requestCurrentTime();
165
- const expirationTimeMs = expirationTimeToMs(renderExpirationTime);
166
- const currentTimeMs = expirationTimeToMs(currentTime);
167
- const startTimeMs = expirationTimeMs - 5000;
168
- let elapsedMs = currentTimeMs - startTimeMs;
169
- if (elapsedMs < 0) {
170
- elapsedMs = 0;
171
- }
172
- const remainingTimeMs = expirationTimeMs - currentTimeMs;
173
-
174
- // Find the earliest timeout of all the timeouts in the ancestor path.
175
- // TODO: Alternatively, we could store the earliest timeout on the context
176
- // stack, rather than searching on every suspend.
155
+ // Find the earliest timeout of all the timeouts in the ancestor path. We
156
+ // could avoid this traversal by storing the timeouts on the stack, but we
157
+ // choose not to because we only hit this path if we're IO-bound (i.e. if
158
+ // something suspends). Whereas the stack is used even in the non-IO-
159
+ // bound case.
160
let workInProgress = returnFiber;
161
let earliestTimeoutMs = -1;
179
- searchForEarliestTimeout: do {
162
+ let startTimeMs = -1;
163
+ do {
164
if (workInProgress.tag === TimeoutComponent) {
165
const current = workInProgress.alternate;
182
- if (current !== null && current.memoizedState === true) {
183
- // A parent Timeout already committed in a placeholder state. We
184
- // need to handle this promise immediately. In other words, we
185
- // should never suspend inside a tree that already expired.
186
- earliestTimeoutMs = 0;
187
- break searchForEarliestTimeout;
166
+ if (
167
+ current !== null &&
168
+ current.memoizedState === true &&
169
+ current.stateNode !== null
170
+ ) {
171
+ // Reached a placeholder that already timed out. Each timed out
172
+ // placeholder acts as the root of a new suspense boundary.
173
+
174
+ // Use the time at which the placeholder timed out as the start time
175
+ // for the current render.
176
+ const timedOutAt = current.stateNode.timedOutAt;
177
+ startTimeMs = expirationTimeToMs(timedOutAt);
178
+
179
+ // Do not search any further.
180
+ break;
181
}
182
let timeoutPropMs = workInProgress.pendingProps.ms;
183
if (typeof timeoutPropMs === 'number') {
184
if (timeoutPropMs <= 0) {
185
earliestTimeoutMs = 0;
193
- break searchForEarliestTimeout;
186
} else if (
187
earliestTimeoutMs === -1 ||
188
timeoutPropMs < earliestTimeoutMs
189
) {
190
earliestTimeoutMs = timeoutPropMs;
191
}
200
- } else if (earliestTimeoutMs === -1) {
201
- earliestTimeoutMs = remainingTimeMs;
192
}
193
}
194
workInProgress = workInProgress.return;
195
} while (workInProgress !== null);
196
207
- // Compute the remaining time until the timeout.
208
- const msUntilTimeout = earliestTimeoutMs - elapsedMs;
209
-
210
- if (renderExpirationTime === Never || msUntilTimeout > 0) {
211
- // There's still time remaining.
212
- markTimeout(root, thenable, msUntilTimeout, renderExpirationTime);
213
- const onResolveOrReject = () => {
214
- retrySuspendedRoot(root, renderExpirationTime);
215
- };
216
- thenable.then(onResolveOrReject, onResolveOrReject);
217
- return;
197
+ let absoluteTimeoutMs;
198
+ if (earliestTimeoutMs === -1) {
199
+ // If no explicit threshold is given, default to an abitrarily large
200
+ // value. The actual size doesn't matter because the threshold for the
201
+ // whole tree will be clamped to the expiration time.
202
+ absoluteTimeoutMs = maxSigned31BitInt;
203
} else {
219
- // No time remaining. Need to fallback to placeholder.
220
- // Find the nearest timeout that can be retried.
221
- workInProgress = returnFiber;
222
- do {
223
- switch (workInProgress.tag) {
224
- case HostRoot: {
225
- // The root expired, but no fallback was provided. Throw a
226
- // helpful error.
227
- const message =
228
- renderExpirationTime === Sync
229
- ? 'A synchronous update was suspended, but no fallback UI ' +
230
- 'was provided.'
231
- : 'An update was suspended for longer than the timeout, ' +
232
- 'but no fallback UI was provided.';
233
- value = new Error(message);
234
- break;
235
- }
236
- case TimeoutComponent: {
237
- if ((workInProgress.effectTag & DidCapture) === NoEffect) {
238
- workInProgress.effectTag |= ShouldCapture;
239
- const onResolveOrReject = schedulePing.bind(null, workInProgress);
240
- thenable.then(onResolveOrReject, onResolveOrReject);
241
- return;
242
- }
243
- // Already captured during this render. Continue to the next
244
- // Timeout ancestor.
245
- break;
246
- }
247
- }
248
- workInProgress = workInProgress.return;
249
- } while (workInProgress !== null);
250
- }
251
- } else {
252
- // This is an error.
253
- markError(root);
254
- if (
255
- // Retry (at the same priority) one more time before handling the error.
256
- // The retry will flush synchronously. (Unless we're already rendering
257
- // synchronously, in which case move to the next check.)
258
- (!root.didError && renderExpirationTime !== Sync) ||
259
- // There's lower priority work. If so, it may have the effect of fixing
260
- // the exception that was just thrown.
261
- hasLowerPriorityWork(root, renderExpirationTime)
262
- ) {
263
- return;
204
+ if (startTimeMs === -1) {
205
+ // This suspend happened outside of any already timed-out
206
+ // placeholders. We don't know exactly when the update was scheduled,
207
+ // but we can infer an approximate start time from the expiration
208
+ // time. First, find the earliest uncommitted expiration time in the
209
+ // tree, including work that is suspended. Then subtract the offset
210
+ // used to compute an async update's expiration time. This will cause
211
+ // high priority (interactive) work to expire earlier than neccessary,
212
+ // but we can account for this by adjusting for the Just Noticable
213
+ // Difference.
214
+ const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
215
+ root,
216
+ renderExpirationTime,
217
+ );
218
+ const earliestExpirationTimeMs = expirationTimeToMs(
219
+ earliestExpirationTime,
220
+ );
221
+ startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
222
+ }
223
+ absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
224
}
225
+
226
+ // Mark the earliest timeout in the suspended fiber's ancestor path. After
227
+ // completing the root, we'll take the largest of all the suspended
228
+ // fiber's timeouts and use it to compute a timeout for the whole tree.
229
+ renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);
230
+
231
+ // Schedule the nearest Timeout to re-render using a placeholder.
232
+ workInProgress = returnFiber;
233
+ do {
234
+ if (workInProgress.tag === TimeoutComponent) {
235
+ if ((workInProgress.effectTag & DidCapture) === NoEffect) {
236
+ workInProgress.effectTag |= ShouldCapture;
237
+ // Attach a listener to the promise to "ping" the root and retry.
238
+ const onResolveOrReject = retrySuspendedRoot.bind(
239
+ null,
240
+ root,
241
+ workInProgress,
242
+ renderExpirationTime,
243
+ );
244
+ thenable.then(onResolveOrReject, onResolveOrReject);
245
+ return;
246
+ }
247
+ // Already captured during this render. Continue to the next
248
+ // Timeout ancestor.
249
+ }
250
+ workInProgress = workInProgress.return;
251
+ } while (workInProgress !== null);
252
+ // No Timeout was found. Fallthrough to error mode.
253
+ value = new Error(
254
+ 'An update was suspended, but no placeholder UI was provided.',
255
+ );
256
}
257
258
// We didn't find a boundary that could handle this type of exception. Start
259
// over and traverse parent path again, this time treating the exception
260
// as an error.
261
+ renderDidError(root);
262
value = createCapturedValue(value, sourceFiber);
263
let workInProgress = returnFiber;
264
do {
@@ -337,11 +329,13 @@ function unwindWork(
329
popHostContainer(workInProgress);
330
popTopLevelLegacyContextObject(workInProgress);
331
const effectTag = workInProgress.effectTag;
340
- if (effectTag & ShouldCapture) {
341
- workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
342
- return workInProgress;
343
- }
344
- return null;
332
+ invariant(
333
+ (effectTag & DidCapture) === NoEffect,
334
+ 'The root failed to unmount after an error. This is likely a bug in ' +
335
+ 'React. Please file an issue.',
336
+ );
337
+ workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
338
+ return workInProgress;
339
}
340
case HostComponent: {
341
popHostContext(workInProgress);
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+2
-2
@@ -234,10 +234,10 @@ describe('ReactIncrementalErrorHandling', () => {
234
// Finish the rest of the async work
235
ReactNoop.flushThrough(['Sibling']);
236
237
- // Rendering one more unit of work should be enough to trigger the retry
237
+ // Rendering two more units of work should be enough to trigger the retry
238
// and synchronously throw an error.
239
ops = [];
240
- expect(() => ReactNoop.flushUnitsOfWork(1)).toThrow('oops');
240
+ expect(() => ReactNoop.flushUnitsOfWork(2)).toThrow('oops');
241
expect(ops).toEqual(['Parent', 'BadRender', 'Sibling']);
242
});
243
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+57
-220
@@ -92,6 +92,7 @@ describe('ReactSuspense', () => {
92
</Timeout>
93
);
94
}
95
+
96
it('suspends rendering and continues later', async () => {
97
function Bar(props) {
98
ReactNoop.yield('Bar');
@@ -152,19 +153,19 @@ describe('ReactSuspense', () => {
153
</Fallback>
154
</Fragment>,
155
);
155
- expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Suspend! [B]']);
156
- expect(ReactNoop.getChildren()).toEqual([]);
157
-
158
- // Advance time by enough to timeout both components and commit their placeholders
159
- ReactNoop.expire(4000);
160
- await advanceTimers(4000);
161
-
156
expect(ReactNoop.flush()).toEqual([
157
'Suspend! [A]',
158
'Loading A...',
159
'Suspend! [B]',
160
'Loading B...',
161
]);
162
+ expect(ReactNoop.getChildren()).toEqual([]);
163
+
164
+ // Advance time by enough to timeout both components and commit their placeholders
165
+ ReactNoop.expire(4000);
166
+ await advanceTimers(4000);
167
+
168
+ expect(ReactNoop.flush()).toEqual([]);
169
expect(ReactNoop.getChildren()).toEqual([
170
span('Loading A...'),
171
span('Loading B...'),
@@ -310,12 +311,12 @@ describe('ReactSuspense', () => {
311
}
312
313
ReactNoop.render(<App />);
313
- expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
314
+ expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
315
expect(ReactNoop.getChildren()).toEqual([]);
316
317
ReactNoop.expire(2000);
318
await advanceTimers(2000);
318
- expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
319
+ expect(ReactNoop.flush()).toEqual([]);
320
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
321
322
textResourceShouldFail = true;
@@ -342,7 +343,7 @@ describe('ReactSuspense', () => {
343
errorBoundary.current.reset();
344
cache.invalidate();
345
345
- expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
346
+ expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
347
ReactNoop.expire(3000);
348
await advanceTimers(3000);
349
expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
@@ -443,53 +444,6 @@ describe('ReactSuspense', () => {
444
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
445
});
446
446
- it('coalesces all async updates when in a suspended state', async () => {
447
- ReactNoop.render(
448
- <Fallback>
449
- <AsyncText text="A" />
450
- </Fallback>,
451
- );
452
- ReactNoop.flush();
453
- await advanceTimers(0);
454
- ReactNoop.flush();
455
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
456
-
457
- ReactNoop.render(
458
- <Fallback>
459
- <AsyncText text="B" ms={50} />
460
- </Fallback>,
461
- );
462
- expect(ReactNoop.flush()).toEqual(['Suspend! [B]']);
463
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
464
-
465
- // Advance React's virtual time so that C falls into a new expiration bucket
466
- ReactNoop.expire(1000);
467
- ReactNoop.render(
468
- <Fallback>
469
- <AsyncText text="C" ms={100} />
470
- </Fallback>,
471
- );
472
- expect(ReactNoop.flush()).toEqual([
473
- // Tries C first, since it has a later expiration time
474
- 'Suspend! [C]',
475
- // Does not retry B, because its promise has not resolved yet.
476
- ]);
477
-
478
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
479
-
480
- // Unblock B
481
- await advanceTimers(90);
482
- // Even though B's promise resolved, the view is still suspended because it
483
- // coalesced with C.
484
- expect(ReactNoop.flush()).toEqual(['Promise resolved [B]']);
485
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
486
-
487
- // Unblock C
488
- await advanceTimers(50);
489
- expect(ReactNoop.flush()).toEqual(['Promise resolved [C]', 'C']);
490
- expect(ReactNoop.getChildren()).toEqual([span('C')]);
491
- });
492
-
447
it('forces an expiration after an update times out', async () => {
448
ReactNoop.render(
449
<Fragment>
@@ -503,6 +457,8 @@ describe('ReactSuspense', () => {
457
expect(ReactNoop.flush()).toEqual([
458
// The async child suspends
459
'Suspend! [Async]',
460
+ // Render the placeholder
461
+ 'Loading...',
462
// Continue on the sibling
463
'Sync',
464
]);
@@ -513,13 +469,10 @@ describe('ReactSuspense', () => {
469
// the update, but not by enough to flush the suspending promise.
470
ReactNoop.expire(10000);
471
await advanceTimers(10000);
516
- expect(ReactNoop.flushExpired()).toEqual([
517
- // Still suspended.
518
- 'Suspend! [Async]',
519
- // Now that the update has expired, we render the fallback UI
520
- 'Loading...',
521
- 'Sync',
522
- ]);
472
+ // No additional rendering work is required, since we already prepared
473
+ // the placeholder.
474
+ expect(ReactNoop.flushExpired()).toEqual([]);
475
+ // Should have committed the placeholder.
476
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
477
478
// Once the promise resolves, we render the suspended view
@@ -535,9 +488,9 @@ describe('ReactSuspense', () => {
488
<Fallback timeout={1000} placeholder={<Text text="Loading outer..." />}>
489
<AsyncText text="Outer content" ms={2000} />
490
<Fallback
538
- timeout={3000}
491
+ timeout={2500}
492
placeholder={<Text text="Loading inner..." />}>
540
- <AsyncText text="Inner content" ms={4000} />
493
+ <AsyncText text="Inner content" ms={5000} />
494
</Fallback>
495
</Fallback>
496
</Fragment>,
@@ -548,6 +501,8 @@ describe('ReactSuspense', () => {
501
// The async content suspends
502
'Suspend! [Outer content]',
503
'Suspend! [Inner content]',
504
+ 'Loading inner...',
505
+ 'Loading outer...',
506
]);
507
// The update hasn't expired yet, so we commit nothing.
508
expect(ReactNoop.getChildren()).toEqual([]);
@@ -556,34 +511,36 @@ describe('ReactSuspense', () => {
511
// We should see the outer loading placeholder.
512
ReactNoop.expire(1500);
513
await advanceTimers(1500);
559
- expect(ReactNoop.flush()).toEqual([
560
- 'Sync',
561
- // Still suspended.
562
- 'Suspend! [Outer content]',
563
- 'Suspend! [Inner content]',
564
- // We attempt to fallback to the inner placeholder
565
- 'Loading inner...',
566
- // But the outer content is still suspended, so we need to fallback to
567
- // the outer placeholder.
568
- 'Loading outer...',
569
- ]);
570
-
514
+ expect(ReactNoop.flush()).toEqual([]);
515
expect(ReactNoop.getChildren()).toEqual([
516
span('Sync'),
517
span('Loading outer...'),
518
]);
519
576
- // Resolve the outer content's promise
577
- ReactNoop.expire(1000);
578
- await advanceTimers(1000);
520
+ // Resolve the outer promise.
521
+ ReactNoop.expire(2000);
522
+ await advanceTimers(2000);
523
+ // At this point, 3.5 seconds have elapsed total. The outer placeholder
524
+ // timed out at 1.5 seconds. So, 2 seconds have elapsed since the
525
+ // placeholder timed out. That means we still haven't reached the 2.5 second
526
+ // threshold of the inner placeholder.
527
expect(ReactNoop.flush()).toEqual([
528
'Promise resolved [Outer content]',
529
'Outer content',
582
- // Inner content still hasn't loaded
530
'Suspend! [Inner content]',
531
'Loading inner...',
532
]);
586
- // We should now see the inner fallback UI.
533
+ // Don't commit the inner placeholder yet.
534
+ expect(ReactNoop.getChildren()).toEqual([
535
+ span('Sync'),
536
+ span('Loading outer...'),
537
+ ]);
538
+
539
+ // Expire the inner timeout.
540
+ ReactNoop.expire(500);
541
+ await advanceTimers(500);
542
+ // Now that 2.5 seconds have elapsed since the outer placeholder timed out,
543
+ // we can timeout the inner placeholder.
544
expect(ReactNoop.getChildren()).toEqual([
545
span('Sync'),
546
span('Outer content'),
@@ -591,8 +548,8 @@ describe('ReactSuspense', () => {
548
]);
549
550
// Finally, flush the inner promise. We should see the complete screen.
594
- ReactNoop.expire(3000);
595
- await advanceTimers(3000);
551
+ ReactNoop.expire(1000);
552
+ await advanceTimers(1000);
553
expect(ReactNoop.flush()).toEqual([
554
'Promise resolved [Inner content]',
555
'Inner content',
@@ -669,6 +626,7 @@ describe('ReactSuspense', () => {
626
expect(ReactNoop.flush()).toEqual([
627
// The async child suspends
628
'Suspend! [Async]',
629
+ 'Loading...',
630
// Continue on the sibling
631
'Sync',
632
]);
@@ -680,13 +638,7 @@ describe('ReactSuspense', () => {
638
// expiration time.
639
ReactNoop.expire(2000);
640
await advanceTimers(2000);
683
- expect(ReactNoop.flush()).toEqual([
684
- // Still suspended.
685
- 'Suspend! [Async]',
686
- // Now that the expiration view has timed out, we render the fallback UI
687
- 'Loading...',
688
- 'Sync',
689
- ]);
641
+ expect(ReactNoop.flush()).toEqual([]);
642
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
643
644
// Once the promise resolves, we render the suspended view
@@ -695,28 +647,12 @@ describe('ReactSuspense', () => {
647
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
648
});
649
698
- it('throws a helpful error when a synchronous update is suspended', () => {
650
+ it('throws a helpful error when an update is suspends without a placeholder', () => {
651
expect(() => {
652
ReactNoop.flushSync(() =>
653
ReactNoop.render(<Timeout>{() => <AsyncText text="Async" />}</Timeout>),
654
);
703
- }).toThrow(
704
- 'A synchronous update was suspended, but no fallback UI was provided.',
705
- );
706
- });
707
-
708
- it('throws a helpful error when an expired update is suspended', async () => {
709
- ReactNoop.render(
710
- <Timeout>{() => <AsyncText text="Async" ms={20000} />}</Timeout>,
711
- );
712
- expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
713
- await advanceTimers(10000);
714
- expect(() => {
715
- ReactNoop.expire(10000);
716
- }).toThrow(
717
- 'An update was suspended for longer than the timeout, but no fallback ' +
718
- 'UI was provided.',
719
- );
655
+ }).toThrow('An update was suspended, but no placeholder UI was provided.');
656
});
657
658
it('a Timeout component correctly handles more than one suspended child', async () => {
@@ -746,7 +682,7 @@ describe('ReactSuspense', () => {
682
<AsyncText text="Async" ms={100} />
683
</Fallback>,
684
);
749
- expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
685
+ expect(ReactNoop.flush()).toEqual(['Suspend! [Async]', 'Loading...']);
686
expect(ReactNoop.getChildren()).toEqual([]);
687
688
// Advance time by an amount slightly smaller than what's necessary to
@@ -830,16 +766,12 @@ describe('ReactSuspense', () => {
766
}
767
768
ReactNoop.render(<App />);
833
- expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
769
+ expect(ReactNoop.flush()).toEqual(['Suspend! [Async]', 'Loading...']);
770
expect(ReactNoop.getChildren()).toEqual([]);
771
772
ReactNoop.expire(2000);
773
await advanceTimers(2000);
838
- expect(ReactNoop.flush()).toEqual([
839
- 'Suspend! [Async]',
840
- 'Loading...',
841
- 'Suspend! [Async]',
842
- ]);
774
+ expect(ReactNoop.flush()).toEqual([]);
775
expect(ReactNoop.getChildren()).toEqual([div(), span('Loading...')]);
776
777
ReactNoop.expire(1000);
@@ -849,109 +781,6 @@ describe('ReactSuspense', () => {
781
expect(ReactNoop.getChildren()).toEqual([div(span('Async'))]);
782
});
783
852
- describe('splitting a high-pri update into high and low', () => {
853
- React = require('react');
854
-
855
- class AsyncValue extends React.Component {
856
- state = {asyncValue: this.props.defaultValue};
857
- componentDidMount() {
858
- ReactNoop.deferredUpdates(() => {
859
- this.setState((state, props) => ({asyncValue: props.value}));
860
- });
861
- }
862
- componentDidUpdate() {
863
- if (this.props.value !== this.state.asyncValue) {
864
- ReactNoop.deferredUpdates(() => {
865
- this.setState((state, props) => ({asyncValue: props.value}));
866
- });
867
- }
868
- }
869
- render() {
870
- return this.props.children(this.state.asyncValue);
871
- }
872
- }
873
-
874
- it('coalesces async values when in a suspended state', async () => {
875
- function App(props) {
876
- const highPriText = props.text;
877
- return (
878
- <Fallback>
879
- <AsyncValue value={highPriText} defaultValue={null}>
880
- {lowPriText => (
881
- <Fragment>
882
- <Text text={`High-pri: ${highPriText}`} />
883
- {lowPriText && (
884
- <AsyncText text={`Low-pri: ${lowPriText}`} ms={100} />
885
- )}
886
- </Fragment>
887
- )}
888
- </AsyncValue>
889
- </Fallback>
890
- );
891
- }
892
-
893
- function renderAppSync(props) {
894
- ReactNoop.flushSync(() => ReactNoop.render(<App {...props} />));
895
- }
896
-
897
- // Initial mount
898
- renderAppSync({text: 'A'});
899
- expect(ReactNoop.flush()).toEqual([
900
- // First we render at high priority
901
- 'High-pri: A',
902
- // Then we come back later to render a low priority
903
- 'High-pri: A',
904
- // The low-pri view suspends
905
- 'Suspend! [Low-pri: A]',
906
- ]);
907
- expect(ReactNoop.getChildren()).toEqual([span('High-pri: A')]);
908
-
909
- // Partially flush the promise for 'A', not by enough to resolve it.
910
- await advanceTimers(99);
911
-
912
- // Advance React's virtual time so that the next update falls into a new
913
- // expiration bucket
914
- ReactNoop.expire(2000);
915
- // Update to B. At this point, the low-pri view still hasn't updated
916
- // to 'A'.
917
- renderAppSync({text: 'B'});
918
- expect(ReactNoop.flush()).toEqual([
919
- // First we render at high priority
920
- 'High-pri: B',
921
- // Then we come back later to render a low priority
922
- 'High-pri: B',
923
- // The low-pri view suspends
924
- 'Suspend! [Low-pri: B]',
925
- ]);
926
- expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]);
927
-
928
- // Flush the rest of the promise for 'A', without flushing the one
929
- // for 'B'.
930
- await advanceTimers(1);
931
- expect(ReactNoop.flush()).toEqual([
932
- // A is unblocked
933
- 'Promise resolved [Low-pri: A]',
934
- // But we don't try to render it, because there's a lower priority
935
- // update that is also suspended.
936
- ]);
937
- expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]);
938
-
939
- // Flush the remaining work.
940
- await advanceTimers(99);
941
- expect(ReactNoop.flush()).toEqual([
942
- // B is unblocked
943
- 'Promise resolved [Low-pri: B]',
944
- // Now we can continue rendering the async view
945
- 'High-pri: B',
946
- 'Low-pri: B',
947
- ]);
948
- expect(ReactNoop.getChildren()).toEqual([
949
- span('High-pri: B'),
950
- span('Low-pri: B'),
951
- ]);
952
- });
953
- });
954
-
784
describe('a Delay component', () => {
785
function Never() {
786
// Throws a promise that resolves after some arbitrarily large
@@ -1001,3 +830,11 @@ describe('ReactSuspense', () => {
830
});
831
});
832
});
833
+
834
+// TODO:
835
+// An update suspends, timeout is scheduled. Update again with different timeout.
836
+// An update suspends, a higher priority update also suspends, each has different timeouts.
837
+// Can update siblings of a timed out placeholder without suspending
838
+// Pinging during the render phase
839
+// Synchronous thenable
840
+// Start time is computed using earliest suspended time
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+7
-3
@@ -288,10 +288,14 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
288
// Stop on Baddie and restart from Boundary
289
⚛ (React Tree Reconciliation: Yielded)
290
⚛ Parent [mount]
291
- ⚛ Boundary [mount]
291
+ ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
292
⚛ Parent [mount]
293
⚛ Baddie [mount]
294
295
+⚛ (React Tree Reconciliation: Completed Root)
296
+ ⚛ Parent [mount]
297
+ ⚛ Boundary [mount]
298
+
299
⚛ (React Tree Reconciliation: Yielded)
300
⚛ Parent [mount]
301
⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
@@ -326,6 +330,8 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
330
⚛ Parent [mount]
331
⚛ Baddie [mount]
332
333
+⚛ (React Tree Reconciliation: Completed Root)
334
+
335
⚛ (React Tree Reconciliation: Yielded)
336
⚛ Parent [mount]
337
⚛ Baddie [mount]
@@ -406,8 +412,6 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
412
exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
413
"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
414
409
-⚛ (React Tree Reconciliation: Yielded)
410
-
415
⚛ (React Tree Reconciliation: Completed Root)
416
⚛ Foo [mount]
417
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+5
@@ -34,6 +34,8 @@ export opaque type PublicInstance = mixed; // eslint-disable-line no-undef
34
export opaque type HostContext = mixed; // eslint-disable-line no-undef
35
export opaque type UpdatePayload = mixed; // eslint-disable-line no-undef
36
export opaque type ChildSet = mixed; // eslint-disable-line no-undef
37
+export opaque type TimeoutHandle = mixed; // eslint-disable-line no-undef
38
+export opaque type NoTimeout = mixed; // eslint-disable-line no-undef
39
40
export const getPublicInstance = $$$hostConfig.getPublicInstance;
41
export const getRootHostContext = $$$hostConfig.getRootHostContext;
@@ -50,6 +52,9 @@ export const shouldDeprioritizeSubtree =
52
export const createTextInstance = $$$hostConfig.createTextInstance;
53
export const scheduleDeferredCallback = $$$hostConfig.scheduleDeferredCallback;
54
export const cancelDeferredCallback = $$$hostConfig.cancelDeferredCallback;
55
+export const scheduleTimeout = $$$hostConfig.setTimeout;
56
+export const cancelTimeout = $$$hostConfig.clearTimeout;
57
+export const noTimeout = $$$hostConfig.noTimeout;
58
export const now = $$$hostConfig.now;
59
export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer;
60
export const supportsMutation = $$$hostConfig.supportsMutation;
packages/react-test-renderer/src/ReactTestHostConfig.js
+6
@@ -32,6 +32,8 @@ export type PublicInstance = Instance | TextInstance;
32
export type HostContext = Object;
33
export type UpdatePayload = Object;
34
export type ChildSet = void; // Unused
35
+export type TimeoutHandle = TimeoutID;
36
+export type NoTimeout = -1;
37
38
export * from 'shared/HostConfigWithNoPersistence';
39
export * from 'shared/HostConfigWithNoHydration';
@@ -187,6 +189,10 @@ export const scheduleDeferredCallback =
189
export const cancelDeferredCallback =
190
TestRendererScheduling.cancelDeferredCallback;
191
192
+export const scheduleTimeout = setTimeout;
193
+export const cancelTimeout = clearTimeout;
194
+export const noTimeout = -1;
195
+
196
// -------------------
197
// Mutation
198
// -------------------