@samitouri / QOS-React / commits / 6565795377

Suspense (#12279)

* Timeout component Adds Timeout component. If a promise is thrown from inside a Timeout component, React will suspend the in-progress render from committing. When the promise resolves, React will retry. If the render is suspended for longer than the maximum threshold, the Timeout switches to a placeholder state. The timeout threshold is defined as the minimum of: - The expiration time of the current render - The `ms` prop given to each Timeout component in the ancestor path of the thrown promise. * Add a test for nested fallbacks Co-authored-by: Andrew Clark <acdlite@fb.com> * Resume on promise rejection React should resume rendering regardless of whether it resolves or rejects. * Wrap Suspense code in feature flag * Children of a Timeout must be strict mode compatible Async is not required for Suspense, but strict mode is. * Simplify list of pending work Some of this was added with "soft expiration" in mind, but now with our revised model for how soft expiration will work, this isn't necessary. It would be nice to remove more of this, but I think the list itself is inherent because we need a way to track the start times, for <Timeout ms={ms} />. * Only use the Timeout update queue to store promises, not for state It already worked this way in practice. * Wrap more Suspense-only paths in the feature flag * Attach promise listener immediately on suspend Instead of waiting for commit phase. * Infer approximate start time using expiration time * Remove list of pending priority levels We can replicate almost all the functionality by tracking just five separate levels: the highest/lowest priority pending levels, the highest/lowest priority suspended levels, and the lowest pinged level. We lose a bit of granularity, in that if there are multiple levels of pending updates, only the first and last ones are known. But in practice this likely isn't a big deal. These heuristics are almost entirely isolated to a single module and can be adjusted later, without API changes, if necessary. Non-IO-bound work is not affected at all. * ReactFiberPendingWork -> ReactFiberPendingPriority * Renaming method names from "pending work" to "pending priority" * Get rid of SuspenseThenable module Idk why I thought this was neccessary * Nits based on Sebastian's feedback * More naming nits + comments * Add test for hiding a suspended tree to unblock * Revert change to expiration time rounding This means you have to account for the start time approximation heuristic when writing Suspense tests, but that's going to be true regardless. When updating the tests, I also made a fix related to offscreen priority. We should never timeout inside a hidden tree. * palceholder -> placeholder

Andrew Clark committed May 10, 2018 at 18:09 UTC 6565795377d1d2c79a7708766f1af9e1a87517de
25 files changed +1561 -76
packages/react-reconciler/src/ReactFiber.js
+8
@@ -32,6 +32,7 @@ import {
32 ContextProvider,
33 ContextConsumer,
34 Profiler,
35 + TimeoutComponent,
36 } from 'shared/ReactTypeOfWork';
37 import getComponentName from 'shared/getComponentName';
38
@@ -47,6 +48,7 @@ import {
48 REACT_PROVIDER_TYPE,
49 REACT_CONTEXT_TYPE,
50 REACT_ASYNC_MODE_TYPE,
51 + REACT_TIMEOUT_TYPE,
52 } from 'shared/ReactSymbols';
53
54 let hasBadMapPolyfill;
@@ -368,6 +370,12 @@ export function createFiberFromElement(
370 case REACT_RETURN_TYPE:
371 fiberTag = ReturnComponent;
372 break;
373 + case REACT_TIMEOUT_TYPE:
374 + fiberTag = TimeoutComponent;
375 + // Suspense does not require async, but its children should be strict
376 + // mode compatible.
377 + mode |= StrictMode;
378 + break;
379 default: {
380 if (typeof type === 'object' && type !== null) {
381 switch (type.$$typeof) {
packages/react-reconciler/src/ReactFiberBeginWork.js
+50 -2
@@ -36,19 +36,21 @@ import {
36 ContextProvider,
37 ContextConsumer,
38 Profiler,
39 + TimeoutComponent,
40 } from 'shared/ReactTypeOfWork';
41 import {
42 NoEffect,
43 PerformedWork,
44 Placement,
45 ContentReset,
45 - Ref,
46 DidCapture,
47 Update,
48 + Ref,
49 } from 'shared/ReactTypeOfSideEffect';
50 import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
51 import {
52 enableGetDerivedStateFromCatch,
53 + enableSuspense,
54 debugRenderPhaseSideEffects,
55 debugRenderPhaseSideEffectsForStrictMode,
56 enableProfilerTimer,
@@ -91,8 +93,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
93 newContext: NewContext,
94 hydrationContext: HydrationContext<C, CX>,
95 scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
94 - computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
96 + computeExpirationForFiber: (
97 + startTime: ExpirationTime,
98 + fiber: Fiber,
99 + ) => ExpirationTime,
100 profilerTimer: ProfilerTimer,
101 + recalculateCurrentTime: () => ExpirationTime,
102 ) {
103 const {shouldSetTextContent, shouldDeprioritizeSubtree} = config;
104
@@ -132,6 +138,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
138 computeExpirationForFiber,
139 memoizeProps,
140 memoizeState,
141 + recalculateCurrentTime,
142 );
143
144 // TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
@@ -758,6 +765,41 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
765 return workInProgress.stateNode;
766 }
767
768 + function updateTimeoutComponent(
769 + current,
770 + workInProgress,
771 + renderExpirationTime,
772 + ) {
773 + if (enableSuspense) {
774 + const nextProps = workInProgress.pendingProps;
775 + const prevProps = workInProgress.memoizedProps;
776 +
777 + const prevDidTimeout = workInProgress.memoizedState;
778 +
779 + // Check if we already attempted to render the normal state. If we did,
780 + // and we timed out, render the placeholder state.
781 + const alreadyCaptured =
782 + (workInProgress.effectTag & DidCapture) === NoEffect;
783 + const nextDidTimeout = !alreadyCaptured;
784 +
785 + if (hasLegacyContextChanged()) {
786 + // Normally we can bail out on props equality but if context has changed
787 + // we don't do the bailout and we have to reuse existing props instead.
788 + } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) {
789 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
790 + }
791 +
792 + const render = nextProps.children;
793 + const nextChildren = render(nextDidTimeout);
794 + workInProgress.memoizedProps = nextProps;
795 + workInProgress.memoizedState = nextDidTimeout;
796 + reconcileChildren(current, workInProgress, nextChildren);
797 + return workInProgress.child;
798 + } else {
799 + return null;
800 + }
801 + }
802 +
803 function updatePortalComponent(
804 current,
805 workInProgress,
@@ -1209,6 +1251,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1251 // A return component is just a placeholder, we can just run through the
1252 // next one immediately.
1253 return null;
1254 + case TimeoutComponent:
1255 + return updateTimeoutComponent(
1256 + current,
1257 + workInProgress,
1258 + renderExpirationTime,
1259 + );
1260 case HostPortal:
1261 return updatePortalComponent(
1262 current,
packages/react-reconciler/src/ReactFiberClassComponent.js
+11 -4
@@ -152,9 +152,13 @@ export function applyDerivedStateFromProps(
152 export default function(
153 legacyContext: LegacyContext,
154 scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
155 - computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
155 + computeExpirationForFiber: (
156 + currentTime: ExpirationTime,
157 + fiber: Fiber,
158 + ) => ExpirationTime,
159 memoizeProps: (workInProgress: Fiber, props: any) => void,
160 memoizeState: (workInProgress: Fiber, state: any) => void,
161 + recalculateCurrentTime: () => ExpirationTime,
162 ) {
163 const {
164 cacheContext,
@@ -168,7 +172,8 @@ export default function(
172 isMounted,
173 enqueueSetState(inst, payload, callback) {
174 const fiber = ReactInstanceMap.get(inst);
171 - const expirationTime = computeExpirationForFiber(fiber);
175 + const currentTime = recalculateCurrentTime();
176 + const expirationTime = computeExpirationForFiber(currentTime, fiber);
177
178 const update = createUpdate(expirationTime);
179 update.payload = payload;
@@ -184,7 +189,8 @@ export default function(
189 },
190 enqueueReplaceState(inst, payload, callback) {
191 const fiber = ReactInstanceMap.get(inst);
187 - const expirationTime = computeExpirationForFiber(fiber);
192 + const currentTime = recalculateCurrentTime();
193 + const expirationTime = computeExpirationForFiber(currentTime, fiber);
194
195 const update = createUpdate(expirationTime);
196 update.tag = ReplaceState;
@@ -202,7 +208,8 @@ export default function(
208 },
209 enqueueForceUpdate(inst, callback) {
210 const fiber = ReactInstanceMap.get(inst);
205 - const expirationTime = computeExpirationForFiber(fiber);
211 + const currentTime = recalculateCurrentTime();
212 + const expirationTime = computeExpirationForFiber(currentTime, fiber);
213
214 const update = createUpdate(expirationTime);
215 update.tag = ForceUpdate;
packages/react-reconciler/src/ReactFiberCommitWork.js
+8
@@ -27,6 +27,7 @@ import {
27 HostPortal,
28 CallComponent,
29 Profiler,
30 + TimeoutComponent,
31 } from 'shared/ReactTypeOfWork';
32 import ReactErrorUtils from 'shared/ReactErrorUtils';
33 import {
@@ -314,6 +315,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
315 // We have no life-cycles associated with Profiler.
316 return;
317 }
318 + case TimeoutComponent: {
319 + // We have no life-cycles associated with Timeouts.
320 + return;
321 + }
322 default: {
323 invariant(
324 false,
@@ -836,6 +841,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
841 }
842 return;
843 }
844 + case TimeoutComponent: {
845 + return;
846 + }
847 default: {
848 invariant(
849 false,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+3
@@ -40,6 +40,7 @@ import {
40 Fragment,
41 Mode,
42 Profiler,
43 + TimeoutComponent,
44 } from 'shared/ReactTypeOfWork';
45 import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
46 import invariant from 'fbjs/lib/invariant';
@@ -593,6 +594,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
594 return null;
595 case ForwardRef:
596 return null;
597 + case TimeoutComponent:
598 + return null;
599 case Fragment:
600 return null;
601 case Mode:
packages/react-reconciler/src/ReactFiberExpirationTime.js
+6 -3
@@ -38,8 +38,11 @@ export function computeExpirationBucket(
38 expirationInMs: number,
39 bucketSizeMs: number,
40 ): ExpirationTime {
41 - return ceiling(
42 - currentTime + expirationInMs / UNIT_SIZE,
43 - bucketSizeMs / UNIT_SIZE,
41 + return (
42 + MAGIC_NUMBER_OFFSET +
43 + ceiling(
44 + currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
45 + bucketSizeMs / UNIT_SIZE,
46 + )
47 );
48 }
packages/react-reconciler/src/ReactFiberPendingPriority.js new
+196
@@ -0,0 +1,196 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {FiberRoot} from './ReactFiberRoot';
11 +import type {ExpirationTime} from './ReactFiberExpirationTime';
12 +
13 +import {NoWork} from './ReactFiberExpirationTime';
14 +
15 +import {enableSuspense} from 'shared/ReactFeatureFlags';
16 +
17 +// TODO: Offscreen updates
18 +
19 +export function markPendingPriorityLevel(
20 + root: FiberRoot,
21 + expirationTime: ExpirationTime,
22 +): void {
23 + if (enableSuspense) {
24 + // Update the latest and earliest pending times
25 + const earliestPendingTime = root.earliestPendingTime;
26 + if (earliestPendingTime === NoWork) {
27 + // No other pending updates.
28 + root.earliestPendingTime = root.latestPendingTime = expirationTime;
29 + } else {
30 + if (earliestPendingTime > expirationTime) {
31 + // This is the earliest pending update.
32 + root.earliestPendingTime = expirationTime;
33 + } else {
34 + const latestPendingTime = root.latestPendingTime;
35 + if (latestPendingTime < expirationTime) {
36 + // This is the latest pending update
37 + root.latestPendingTime = expirationTime;
38 + }
39 + }
40 + }
41 + }
42 +}
43 +
44 +export function markCommittedPriorityLevels(
45 + root: FiberRoot,
46 + currentTime: ExpirationTime,
47 + earliestRemainingTime: ExpirationTime,
48 +): void {
49 + if (enableSuspense) {
50 + if (earliestRemainingTime === NoWork) {
51 + // Fast path. There's no remaining work. Clear everything.
52 + root.earliestPendingTime = NoWork;
53 + root.latestPendingTime = NoWork;
54 + root.earliestSuspendedTime = NoWork;
55 + root.latestSuspendedTime = NoWork;
56 + root.latestPingedTime = NoWork;
57 + return;
58 + }
59 +
60 + // Let's see if the previous latest known pending level was just flushed.
61 + const latestPendingTime = root.latestPendingTime;
62 + if (latestPendingTime !== NoWork) {
63 + if (latestPendingTime < earliestRemainingTime) {
64 + // We've flushed all the known pending levels.
65 + root.earliestPendingTime = root.latestPendingTime = NoWork;
66 + } else {
67 + const earliestPendingTime = root.earliestPendingTime;
68 + if (earliestPendingTime < earliestRemainingTime) {
69 + // We've flushed the earliest known pending level. Set this to the
70 + // latest pending time.
71 + root.earliestPendingTime = root.latestPendingTime;
72 + }
73 + }
74 + }
75 +
76 + // Now let's handle the earliest remaining level in the whole tree. We need to
77 + // decide whether to treat it as a pending level or as suspended. Check
78 + // it falls within the range of known suspended levels.
79 +
80 + const earliestSuspendedTime = root.earliestSuspendedTime;
81 + if (earliestSuspendedTime === NoWork) {
82 + // There's no suspended work. Treat the earliest remaining level as a
83 + // pending level.
84 + markPendingPriorityLevel(root, earliestRemainingTime);
85 + return;
86 + }
87 +
88 + const latestSuspendedTime = root.latestSuspendedTime;
89 + if (earliestRemainingTime > latestSuspendedTime) {
90 + // The earliest remaining level is later than all the suspended work. That
91 + // means we've flushed all the suspended work.
92 + root.earliestSuspendedTime = NoWork;
93 + root.latestSuspendedTime = NoWork;
94 + root.latestPingedTime = NoWork;
95 +
96 + // There's no suspended work. Treat the earliest remaining level as a
97 + // pending level.
98 + markPendingPriorityLevel(root, earliestRemainingTime);
99 + return;
100 + }
101 +
102 + if (earliestRemainingTime < earliestSuspendedTime) {
103 + // The earliest remaining time is earlier than all the suspended work.
104 + // Treat it as a pending update.
105 + markPendingPriorityLevel(root, earliestRemainingTime);
106 + return;
107 + }
108 +
109 + // The earliest remaining time falls within the range of known suspended
110 + // levels. We should treat this as suspended work.
111 + }
112 +}
113 +
114 +export function markSuspendedPriorityLevel(
115 + root: FiberRoot,
116 + suspendedTime: ExpirationTime,
117 +): void {
118 + if (enableSuspense) {
119 + // First, check the known pending levels and update them if needed.
120 + const earliestPendingTime = root.earliestPendingTime;
121 + const latestPendingTime = root.latestPendingTime;
122 + if (earliestPendingTime === suspendedTime) {
123 + if (latestPendingTime === suspendedTime) {
124 + // Both known pending levels were suspended. Clear them.
125 + root.earliestPendingTime = root.latestPendingTime = NoWork;
126 + } else {
127 + // The earliest pending level was suspended. Clear by setting it to the
128 + // latest pending level.
129 + root.earliestPendingTime = latestPendingTime;
130 + }
131 + } else if (latestPendingTime === suspendedTime) {
132 + // The latest pending level was suspended. Clear by setting it to the
133 + // latest pending level.
134 + root.latestPendingTime = earliestPendingTime;
135 + }
136 +
137 + // Next, if we're working on the lowest known suspended level, clear the ping.
138 + // TODO: What if a promise suspends and pings before the root completes?
139 + const latestSuspendedTime = root.latestSuspendedTime;
140 + if (latestSuspendedTime === suspendedTime) {
141 + root.latestPingedTime = NoWork;
142 + }
143 +
144 + // Finally, update the known suspended levels.
145 + const earliestSuspendedTime = root.earliestSuspendedTime;
146 + if (earliestSuspendedTime === NoWork) {
147 + // No other suspended levels.
148 + root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;
149 + } else {
150 + if (earliestSuspendedTime > suspendedTime) {
151 + // This is the earliest suspended level.
152 + root.earliestSuspendedTime = suspendedTime;
153 + } else if (latestSuspendedTime < suspendedTime) {
154 + // This is the latest suspended level
155 + root.latestSuspendedTime = suspendedTime;
156 + }
157 + }
158 + }
159 +}
160 +
161 +export function markPingedPriorityLevel(
162 + root: FiberRoot,
163 + pingedTime: ExpirationTime,
164 +): void {
165 + if (enableSuspense) {
166 + const latestSuspendedTime = root.latestSuspendedTime;
167 + if (latestSuspendedTime !== NoWork && latestSuspendedTime <= pingedTime) {
168 + const latestPingedTime = root.latestPingedTime;
169 + if (latestPingedTime === NoWork || latestPingedTime < pingedTime) {
170 + root.latestPingedTime = pingedTime;
171 + }
172 + }
173 + }
174 +}
175 +
176 +export function findNextPendingPriorityLevel(root: FiberRoot): ExpirationTime {
177 + if (enableSuspense) {
178 + const earliestSuspendedTime = root.earliestSuspendedTime;
179 + const earliestPendingTime = root.earliestPendingTime;
180 + if (earliestSuspendedTime === NoWork) {
181 + // Fast path. There's no suspended work.
182 + return earliestPendingTime;
183 + }
184 +
185 + // First, check if there's known pending work.
186 + if (earliestPendingTime !== NoWork) {
187 + return earliestPendingTime;
188 + }
189 +
190 + // Finally, if a suspended level was pinged, work on that. Otherwise there's
191 + // nothing to work on.
192 + return root.latestPingedTime;
193 + } else {
194 + return root.current.expirationTime;
195 + }
196 +}
packages/react-reconciler/src/ReactFiberReconciler.js
+2 -13
@@ -317,7 +317,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
317 function scheduleRootUpdate(
318 current: Fiber,
319 element: ReactNodeList,
320 - currentTime: ExpirationTime,
320 expirationTime: ExpirationTime,
321 callback: ?Function,
322 ) {
@@ -364,7 +363,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
363 element: ReactNodeList,
364 container: OpaqueRoot,
365 parentComponent: ?React$Component<any, any>,
367 - currentTime: ExpirationTime,
366 expirationTime: ExpirationTime,
367 callback: ?Function,
368 ) {
@@ -390,13 +388,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
388 container.pendingContext = context;
389 }
390
393 - return scheduleRootUpdate(
394 - current,
395 - element,
396 - currentTime,
397 - expirationTime,
398 - callback,
399 - );
391 + return scheduleRootUpdate(current, element, expirationTime, callback);
392 }
393
394 function findHostInstance(component: Object): PI | null {
@@ -436,12 +428,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
428 ): ExpirationTime {
429 const current = container.current;
430 const currentTime = recalculateCurrentTime();
439 - const expirationTime = computeExpirationForFiber(current);
431 + const expirationTime = computeExpirationForFiber(currentTime, current);
432 return updateContainerAtExpirationTime(
433 element,
434 container,
435 parentComponent,
444 - currentTime,
436 expirationTime,
437 callback,
438 );
@@ -454,12 +445,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
445 expirationTime,
446 callback,
447 ) {
457 - const currentTime = recalculateCurrentTime();
448 return updateContainerAtExpirationTime(
449 element,
450 container,
451 parentComponent,
462 - currentTime,
452 expirationTime,
453 callback,
454 );
packages/react-reconciler/src/ReactFiberRoot.js
+23
@@ -28,6 +28,22 @@ export type FiberRoot = {
28 pendingChildren: any,
29 // The currently active root fiber. This is the mutable root of the tree.
30 current: Fiber,
31 +
32 + // The following priority levels are used to distinguish between 1)
33 + // uncommitted work, 2) uncommitted work that is suspended, and 3) uncommitted
34 + // work that may be unsuspended. We choose not to track each individual
35 + // pending level, trading granularity for performance.
36 + //
37 + // The earliest and latest priority levels that are suspended from committing.
38 + earliestSuspendedTime: ExpirationTime,
39 + latestSuspendedTime: ExpirationTime,
40 + // The earliest and latest priority levels that are not known to be suspended.
41 + earliestPendingTime: ExpirationTime,
42 + latestPendingTime: ExpirationTime,
43 + // The latest priority level that was pinged by a resolved promise and can
44 + // be retried.
45 + latestPingedTime: ExpirationTime,
46 +
47 pendingCommitExpirationTime: ExpirationTime,
48 // A finished work-in-progress HostRoot that's ready to be committed.
49 // TODO: The reason this is separate from isReadyForCommit is because the
@@ -62,6 +78,13 @@ export function createFiberRoot(
78 current: uninitializedFiber,
79 containerInfo: containerInfo,
80 pendingChildren: null,
81 +
82 + earliestPendingTime: NoWork,
83 + latestPendingTime: NoWork,
84 + earliestSuspendedTime: NoWork,
85 + latestSuspendedTime: NoWork,
86 + latestPingedTime: NoWork,
87 +
88 pendingCommitExpirationTime: NoWork,
89 finishedWork: null,
90 context: null,
packages/react-reconciler/src/ReactFiberScheduler.js
+109 -19
@@ -58,6 +58,13 @@ import ReactFiberHostContext from './ReactFiberHostContext';
58 import ReactFiberHydrationContext from './ReactFiberHydrationContext';
59 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
60 import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
61 +import {
62 + markPendingPriorityLevel,
63 + markCommittedPriorityLevels,
64 + findNextPendingPriorityLevel,
65 + markSuspendedPriorityLevel,
66 + markPingedPriorityLevel,
67 +} from './ReactFiberPendingPriority';
68 import {
69 recordEffect,
70 recordScheduleUpdate,
@@ -94,6 +101,10 @@ import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
101 import {createCapturedValue} from './ReactCapturedValue';
102 import ReactFiberStack from './ReactFiberStack';
103
104 +export type Thenable = {
105 + then(resolve: () => mixed, reject?: () => mixed): mixed,
106 +};
107 +
108 const {
109 invokeGuardedCallback,
110 hasCaughtError,
@@ -190,6 +201,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
201 scheduleWork,
202 computeExpirationForFiber,
203 profilerTimer,
204 + recalculateCurrentTime,
205 );
206 const {completeWork} = ReactFiberCompleteWork(
207 config,
@@ -211,10 +223,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
223 legacyContext,
224 newContext,
225 scheduleWork,
226 + computeExpirationForFiber,
227 + recalculateCurrentTime,
228 markLegacyErrorBoundaryAsFailed,
229 isAlreadyFailedLegacyErrorBoundary,
230 onUncaughtError,
231 profilerTimer,
232 + suspendRoot,
233 + retrySuspendedRoot,
234 );
235 const {
236 commitBeforeMutationLifeCycles,
@@ -264,6 +280,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
280 let nextRoot: FiberRoot | null = null;
281 // The time at which we're currently rendering work.
282 let nextRenderExpirationTime: ExpirationTime = NoWork;
283 + let nextLatestTimeoutMs: number = -1;
284 + let nextRenderIsExpired: boolean = false;
285
286 // The next fiber with an effect that we're currently committing.
287 let nextEffect: Fiber | null = null;
@@ -288,9 +306,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
306 originalReplayError = null;
307 replayUnitOfWork = (
308 failedUnitOfWork: Fiber,
291 - error: mixed,
309 + thrownValue: mixed,
310 isAsync: boolean,
311 ) => {
312 + if (
313 + thrownValue !== null &&
314 + typeof thrownValue === 'object' &&
315 + typeof thrownValue.then === 'function'
316 + ) {
317 + // Don't replay promises. Treat everything else like an error.
318 + // TODO: Need to figure out a different strategy if/when we add
319 + // support for catching other types.
320 + return;
321 + }
322 +
323 // Restore the original state of the work-in-progress
324 assignFiberPropertiesInDEV(
325 failedUnitOfWork,
@@ -316,7 +345,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
345 }
346 // Replay the begin phase.
347 isReplayingFailedUnitOfWork = true;
319 - originalReplayError = error;
348 + originalReplayError = thrownValue;
349 invokeGuardedCallback(null, workLoop, null, isAsync);
350 isReplayingFailedUnitOfWork = false;
351 originalReplayError = null;
@@ -354,6 +383,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
383
384 nextRoot = null;
385 nextRenderExpirationTime = NoWork;
386 + nextLatestTimeoutMs = -1;
387 + nextRenderIsExpired = false;
388 nextUnitOfWork = null;
389
390 isRootReadyForCommit = false;
@@ -685,7 +716,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
716 ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
717 }
718
688 - const remainingTime = root.current.expirationTime;
719 + markCommittedPriorityLevels(root, currentTime, root.current.expirationTime);
720 + const remainingTime = findNextPendingPriorityLevel(root);
721 if (remainingTime === NoWork) {
722 // If there's no remaining work, we can clear the set of already failed
723 // error boundaries.
@@ -849,7 +881,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
881 // This fiber did not complete because something threw. Pop values off
882 // the stack without entering the complete phase. If this is a boundary,
883 // capture values if possible.
852 - const next = unwindWork(workInProgress);
884 + const next = unwindWork(
885 + workInProgress,
886 + nextRenderIsExpired,
887 + nextRenderExpirationTime,
888 + );
889 // Because this fiber did not complete, don't reset its expiration time.
890 if (workInProgress.effectTag & DidCapture) {
891 // Restarting an error boundary
@@ -1003,6 +1039,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1039 resetStack();
1040 nextRoot = root;
1041 nextRenderExpirationTime = expirationTime;
1042 + nextLatestTimeoutMs = -1;
1043 nextUnitOfWork = createWorkInProgress(
1044 nextRoot.current,
1045 null,
@@ -1013,6 +1050,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1050
1051 let didFatal = false;
1052
1053 + nextRenderIsExpired =
1054 + !isAsync || nextRenderExpirationTime <= mostRecentCurrentTime;
1055 +
1056 startWorkLoopTimer(nextUnitOfWork);
1057
1058 do {
@@ -1056,10 +1096,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1096 break;
1097 }
1098 throwException(
1099 + root,
1100 returnFiber,
1101 sourceFiber,
1102 thrownValue,
1103 + nextRenderIsExpired,
1104 nextRenderExpirationTime,
1105 + mostRecentCurrentTimeMs,
1106 );
1107 nextUnitOfWork = completeUnitOfWork(sourceFiber);
1108 }
@@ -1094,10 +1137,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1137 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1138 interruptedBy = null;
1139 invariant(
1097 - false,
1140 + !nextRenderIsExpired,
1141 'Expired work should have completed. This error is likely caused ' +
1142 'by a bug in React. Please file an issue.',
1143 );
1144 + markSuspendedPriorityLevel(root, expirationTime);
1145 + if (nextLatestTimeoutMs >= 0) {
1146 + setTimeout(() => {
1147 + retrySuspendedRoot(root, expirationTime);
1148 + }, nextLatestTimeoutMs);
1149 + }
1150 + const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root);
1151 + onBlock(firstUnblockedExpirationTime);
1152 + return null;
1153 }
1154 } else {
1155 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
@@ -1218,7 +1270,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1270 return lastUniqueAsyncExpiration;
1271 }
1272
1221 - function computeExpirationForFiber(fiber: Fiber) {
1273 + function computeExpirationForFiber(
1274 + currentTime: ExpirationTime,
1275 + fiber: Fiber,
1276 + ) {
1277 let expirationTime;
1278 if (expirationContext !== NoWork) {
1279 // An explicit expiration context was set;
@@ -1239,11 +1294,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1294 if (fiber.mode & AsyncMode) {
1295 if (isBatchingInteractiveUpdates) {
1296 // This is an interactive update
1242 - const currentTime = recalculateCurrentTime();
1297 expirationTime = computeInteractiveExpiration(currentTime);
1298 } else {
1299 // This is an async update
1246 - const currentTime = recalculateCurrentTime();
1300 expirationTime = computeAsyncExpiration(currentTime);
1301 }
1302 } else {
@@ -1265,19 +1318,32 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1318 return expirationTime;
1319 }
1320
1268 - function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1269 - return scheduleWorkImpl(fiber, expirationTime, false);
1321 + // TODO: Rename this to scheduleTimeout or something
1322 + function suspendRoot(
1323 + root: FiberRoot,
1324 + thenable: Thenable,
1325 + timeoutMs: number,
1326 + suspendedTime: ExpirationTime,
1327 + ) {
1328 + // Schedule the timeout.
1329 + if (timeoutMs >= 0 && nextLatestTimeoutMs < timeoutMs) {
1330 + nextLatestTimeoutMs = timeoutMs;
1331 + }
1332 }
1333
1272 - function scheduleWorkImpl(
1273 - fiber: Fiber,
1274 - expirationTime: ExpirationTime,
1275 - isErrorRecovery: boolean,
1276 - ) {
1334 + function retrySuspendedRoot(root, suspendedTime) {
1335 + markPingedPriorityLevel(root, suspendedTime);
1336 + const retryTime = findNextPendingPriorityLevel(root);
1337 + if (retryTime !== NoWork) {
1338 + requestRetry(root, retryTime);
1339 + }
1340 + }
1341 +
1342 + function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1343 recordScheduleUpdate();
1344
1345 if (__DEV__) {
1280 - if (!isErrorRecovery && fiber.tag === ClassComponent) {
1346 + if (fiber.tag === ClassComponent) {
1347 const instance = fiber.stateNode;
1348 warnAboutInvalidUpdates(instance);
1349 }
@@ -1313,6 +1379,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1379 interruptedBy = fiber;
1380 resetStack();
1381 }
1382 + markPendingPriorityLevel(root, expirationTime);
1383 + const nextExpirationTimeToWorkOn = findNextPendingPriorityLevel(root);
1384 if (
1385 // If we're in the render phase, we don't need to schedule this root
1386 // for an update, because we'll do it before we exit...
@@ -1321,8 +1389,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1389 // ...unless this is a different root than the one we're rendering.
1390 nextRoot !== root
1391 ) {
1324 - // Add this root to the root schedule.
1325 - requestWork(root, expirationTime);
1392 + requestWork(root, nextExpirationTimeToWorkOn);
1393 }
1394 if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1395 invariant(
@@ -1335,7 +1402,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1402 }
1403 } else {
1404 if (__DEV__) {
1338 - if (!isErrorRecovery && fiber.tag === ClassComponent) {
1405 + if (fiber.tag === ClassComponent) {
1406 warnAboutUpdateOnUnmounted(fiber);
1407 }
1408 }
@@ -1434,6 +1501,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1501 callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
1502 }
1503
1504 + function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) {
1505 + if (
1506 + root.remainingExpirationTime === NoWork ||
1507 + root.remainingExpirationTime < expirationTime
1508 + ) {
1509 + // For a retry, only update the remaining expiration time if it has a
1510 + // *lower priority* than the existing value. This is because, on a retry,
1511 + // we should attempt to coalesce as much as possible.
1512 + requestWork(root, expirationTime);
1513 + }
1514 + }
1515 +
1516 // requestWork is called by the scheduler whenever a root receives an update.
1517 // It's up to the renderer to call renderRoot at some point in the future.
1518 function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -1606,6 +1685,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1685 (!deadlineDidExpire ||
1686 recalculateCurrentTime() >= nextFlushedExpirationTime)
1687 ) {
1688 + recalculateCurrentTime();
1689 performWorkOnRoot(
1690 nextFlushedRoot,
1691 nextFlushedExpirationTime,
@@ -1807,6 +1887,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1887 }
1888 }
1889
1890 + function onBlock(remainingExpirationTime: ExpirationTime) {
1891 + invariant(
1892 + nextFlushedRoot !== null,
1893 + 'Should be working on a root. This error is likely caused by a bug in ' +
1894 + 'React. Please file an issue.',
1895 + );
1896 + // This root was blocked. Unschedule it until there's another update.
1897 + nextFlushedRoot.remainingExpirationTime = remainingExpirationTime;
1898 + }
1899 +
1900 // TODO: Batching should be implemented at the renderer level, not inside
1901 // the reconciler.
1902 function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
packages/react-reconciler/src/ReactFiberUnwindWork.js
+148 -7
@@ -9,6 +9,7 @@
9
10 import type {HostConfig} from 'react-reconciler';
11 import type {Fiber} from './ReactFiber';
12 +import type {FiberRoot} from './ReactFiberRoot';
13 import type {ExpirationTime} from './ReactFiberExpirationTime';
14 import type {HostContext} from './ReactFiberHostContext';
15 import type {LegacyContext} from './ReactFiberContext';
@@ -16,11 +17,13 @@ import type {NewContext} from './ReactFiberNewContext';
17 import type {CapturedValue} from './ReactCapturedValue';
18 import type {ProfilerTimer} from './ReactProfilerTimer';
19 import type {Update} from './ReactUpdateQueue';
20 +import type {Thenable} from './ReactFiberScheduler';
21
22 import {createCapturedValue} from './ReactCapturedValue';
23 import {
24 enqueueCapturedUpdate,
25 createUpdate,
26 + enqueueUpdate,
27 CaptureUpdate,
28 } from './ReactUpdateQueue';
29 import {logError} from './ReactFiberCommitWork';
@@ -32,6 +35,7 @@ import {
35 HostPortal,
36 ContextProvider,
37 Profiler,
38 + TimeoutComponent,
39 } from 'shared/ReactTypeOfWork';
40 import {
41 DidCapture,
@@ -42,22 +46,33 @@ import {
46 import {
47 enableGetDerivedStateFromCatch,
48 enableProfilerTimer,
49 + enableSuspense,
50 } from 'shared/ReactFeatureFlags';
51
52 +import {Never, Sync, expirationTimeToMs} from './ReactFiberExpirationTime';
53 +
54 export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
55 config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
56 hostContext: HostContext<C, CX>,
57 legacyContext: LegacyContext,
58 newContext: NewContext,
52 - scheduleWork: (
53 - fiber: Fiber,
59 + scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
60 + computeExpirationForFiber: (
61 startTime: ExpirationTime,
55 - expirationTime: ExpirationTime,
56 - ) => void,
62 + fiber: Fiber,
63 + ) => ExpirationTime,
64 + recalculateCurrentTime: () => ExpirationTime,
65 markLegacyErrorBoundaryAsFailed: (instance: mixed) => void,
66 isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean,
67 onUncaughtError: (error: mixed) => void,
68 profilerTimer: ProfilerTimer,
69 + suspendRoot: (
70 + root: FiberRoot,
71 + thenable: Thenable,
72 + timeoutMs: number,
73 + suspendedTime: ExpirationTime,
74 + ) => void,
75 + retrySuspendedRoot: (root: FiberRoot, suspendedTime: ExpirationTime) => void,
76 ) {
77 const {popHostContainer, popHostContext} = hostContext;
78 const {
@@ -132,19 +147,133 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
147 return update;
148 }
149
150 + function schedulePing(finishedWork) {
151 + // Once the promise resolves, we should try rendering the non-
152 + // placeholder state again.
153 + const currentTime = recalculateCurrentTime();
154 + const expirationTime = computeExpirationForFiber(currentTime, finishedWork);
155 + const recoveryUpdate = createUpdate(expirationTime);
156 + enqueueUpdate(finishedWork, recoveryUpdate, expirationTime);
157 + scheduleWork(finishedWork, expirationTime);
158 + }
159 +
160 function throwException(
161 + root: FiberRoot,
162 returnFiber: Fiber,
163 sourceFiber: Fiber,
138 - rawValue: mixed,
164 + value: mixed,
165 + renderIsExpired: boolean,
166 renderExpirationTime: ExpirationTime,
167 + currentTimeMs: number,
168 ) {
169 // The source fiber did not complete.
170 sourceFiber.effectTag |= Incomplete;
171 // Its effect list is no longer valid.
172 sourceFiber.firstEffect = sourceFiber.lastEffect = null;
173
146 - const value = createCapturedValue(rawValue, sourceFiber);
174 + if (
175 + enableSuspense &&
176 + value !== null &&
177 + typeof value === 'object' &&
178 + typeof value.then === 'function'
179 + ) {
180 + // This is a thenable.
181 + const thenable: Thenable = (value: any);
182 +
183 + const expirationTimeMs = expirationTimeToMs(renderExpirationTime);
184 + const startTimeMs = expirationTimeMs - 5000;
185 + let elapsedMs = currentTimeMs - startTimeMs;
186 + if (elapsedMs < 0) {
187 + elapsedMs = 0;
188 + }
189 + const remainingTimeMs = expirationTimeMs - currentTimeMs;
190 +
191 + // Find the earliest timeout of all the timeouts in the ancestor path.
192 + // TODO: Alternatively, we could store the earliest timeout on the context
193 + // stack, rather than searching on every suspend.
194 + let workInProgress = returnFiber;
195 + let earliestTimeoutMs = -1;
196 + searchForEarliestTimeout: do {
197 + if (workInProgress.tag === TimeoutComponent) {
198 + const current = workInProgress.alternate;
199 + if (current !== null && current.memoizedState === true) {
200 + // A parent Timeout already committed in a placeholder state. We
201 + // need to handle this promise immediately. In other words, we
202 + // should never suspend inside a tree that already expired.
203 + earliestTimeoutMs = 0;
204 + break searchForEarliestTimeout;
205 + }
206 + let timeoutPropMs = workInProgress.pendingProps.ms;
207 + if (typeof timeoutPropMs === 'number') {
208 + if (timeoutPropMs <= 0) {
209 + earliestTimeoutMs = 0;
210 + break searchForEarliestTimeout;
211 + } else if (
212 + earliestTimeoutMs === -1 ||
213 + timeoutPropMs < earliestTimeoutMs
214 + ) {
215 + earliestTimeoutMs = timeoutPropMs;
216 + }
217 + } else if (earliestTimeoutMs === -1) {
218 + earliestTimeoutMs = remainingTimeMs;
219 + }
220 + }
221 + workInProgress = workInProgress.return;
222 + } while (workInProgress !== null);
223 +
224 + // Compute the remaining time until the timeout.
225 + const msUntilTimeout = earliestTimeoutMs - elapsedMs;
226 +
227 + if (renderExpirationTime === Never || msUntilTimeout > 0) {
228 + // There's still time remaining.
229 + suspendRoot(root, thenable, msUntilTimeout, renderExpirationTime);
230 + const onResolveOrReject = () => {
231 + retrySuspendedRoot(root, renderExpirationTime);
232 + };
233 + thenable.then(onResolveOrReject, onResolveOrReject);
234 + return;
235 + } else {
236 + // No time remaining. Need to fallback to placeholder.
237 + // Find the nearest timeout that can be retried.
238 + workInProgress = returnFiber;
239 + do {
240 + switch (workInProgress.tag) {
241 + case HostRoot: {
242 + // The root expired, but no fallback was provided. Throw a
243 + // helpful error.
244 + const message =
245 + renderExpirationTime === Sync
246 + ? 'A synchronous update was suspended, but no fallback UI ' +
247 + 'was provided.'
248 + : 'An update was suspended for longer than the timeout, ' +
249 + 'but no fallback UI was provided.';
250 + value = new Error(message);
251 + break;
252 + }
253 + case TimeoutComponent: {
254 + if ((workInProgress.effectTag & DidCapture) === NoEffect) {
255 + workInProgress.effectTag |= ShouldCapture;
256 + const onResolveOrReject = schedulePing.bind(
257 + null,
258 + workInProgress,
259 + );
260 + thenable.then(onResolveOrReject, onResolveOrReject);
261 + return;
262 + }
263 + // Already captured during this render. Continue to the next
264 + // Timeout ancestor.
265 + break;
266 + }
267 + }
268 + workInProgress = workInProgress.return;
269 + } while (workInProgress !== null);
270 + }
271 + }
272
273 + // We didn't find a boundary that could handle this type of exception. Start
274 + // over and traverse parent path again, this time treating the exception
275 + // as an error.
276 + value = createCapturedValue(value, sourceFiber);
277 let workInProgress = returnFiber;
278 do {
279 switch (workInProgress.tag) {
@@ -190,7 +319,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
319 } while (workInProgress !== null);
320 }
321
193 - function unwindWork(workInProgress: Fiber) {
322 + function unwindWork(
323 + workInProgress: Fiber,
324 + renderIsExpired: boolean,
325 + renderExpirationTime: ExpirationTime,
326 + ) {
327 switch (workInProgress.tag) {
328 case ClassComponent: {
329 popLegacyContextProvider(workInProgress);
@@ -215,6 +348,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
348 popHostContext(workInProgress);
349 return null;
350 }
351 + case TimeoutComponent: {
352 + const effectTag = workInProgress.effectTag;
353 + if (effectTag & ShouldCapture) {
354 + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
355 + return workInProgress;
356 + }
357 + return null;
358 + }
359 case HostPortal:
360 popHostContainer(workInProgress);
361 return null;
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js new
+946
@@ -0,0 +1,946 @@
1 +let React;
2 +let ReactFeatureFlags;
3 +let Fragment;
4 +let ReactNoop;
5 +let SimpleCacheProvider;
6 +let Timeout;
7 +
8 +let cache;
9 +let TextResource;
10 +let textResourceShouldFail;
11 +
12 +describe('ReactSuspense', () => {
13 + beforeEach(() => {
14 + jest.resetModules();
15 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
16 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
17 + ReactFeatureFlags.enableSuspense = true;
18 + React = require('react');
19 + Fragment = React.Fragment;
20 + ReactNoop = require('react-noop-renderer');
21 + SimpleCacheProvider = require('simple-cache-provider');
22 + Timeout = React.Timeout;
23 +
24 + function invalidateCache() {
25 + cache = SimpleCacheProvider.createCache(invalidateCache);
26 + }
27 + invalidateCache();
28 + TextResource = SimpleCacheProvider.createResource(([text, ms = 0]) => {
29 + return new Promise((resolve, reject) =>
30 + setTimeout(() => {
31 + if (textResourceShouldFail) {
32 + ReactNoop.yield(`Promise rejected [${text}]`);
33 + reject(new Error('Failed to load: ' + text));
34 + } else {
35 + ReactNoop.yield(`Promise resolved [${text}]`);
36 + resolve(text);
37 + }
38 + }, ms),
39 + );
40 + }, ([text, ms]) => text);
41 + textResourceShouldFail = false;
42 + });
43 +
44 + function div(...children) {
45 + children = children.map(c => (typeof c === 'string' ? {text: c} : c));
46 + return {type: 'div', children, prop: undefined};
47 + }
48 +
49 + function span(prop) {
50 + return {type: 'span', children: [], prop};
51 + }
52 +
53 + function advanceTimers(ms) {
54 + // Note: This advances Jest's virtual time but not React's. Use
55 + // ReactNoop.expire for that.
56 + if (typeof ms !== 'number') {
57 + throw new Error('Must specify ms');
58 + }
59 + jest.advanceTimersByTime(ms);
60 + // Wait until the end of the current tick
61 + return new Promise(resolve => {
62 + setImmediate(resolve);
63 + });
64 + }
65 +
66 + function Text(props) {
67 + ReactNoop.yield(props.text);
68 + return <span prop={props.text} />;
69 + }
70 +
71 + function AsyncText(props) {
72 + const text = props.text;
73 + try {
74 + TextResource.read(cache, [props.text, props.ms]);
75 + ReactNoop.yield(text);
76 + return <span prop={text} />;
77 + } catch (promise) {
78 + if (typeof promise.then === 'function') {
79 + ReactNoop.yield(`Suspend! [${text}]`);
80 + } else {
81 + ReactNoop.yield(`Error! [${text}]`);
82 + }
83 + throw promise;
84 + }
85 + }
86 +
87 + function Fallback(props) {
88 + return (
89 + <Timeout ms={props.timeout}>
90 + {didExpire => (didExpire ? props.placeholder : props.children)}
91 + </Timeout>
92 + );
93 + }
94 + it('suspends rendering and continues later', async () => {
95 + function Bar(props) {
96 + ReactNoop.yield('Bar');
97 + return props.children;
98 + }
99 +
100 + function Foo() {
101 + ReactNoop.yield('Foo');
102 + return (
103 + <Fallback>
104 + <Bar>
105 + <AsyncText text="A" ms={100} />
106 + <Text text="B" />
107 + </Bar>
108 + </Fallback>
109 + );
110 + }
111 +
112 + ReactNoop.render(<Foo />);
113 + expect(ReactNoop.flush()).toEqual([
114 + 'Foo',
115 + 'Bar',
116 + // A suspends
117 + 'Suspend! [A]',
118 + // But we keep rendering the siblings
119 + 'B',
120 + ]);
121 + expect(ReactNoop.getChildren()).toEqual([]);
122 +
123 + // Flush some of the time
124 + await advanceTimers(50);
125 + // Still nothing...
126 + expect(ReactNoop.flush()).toEqual([]);
127 + expect(ReactNoop.getChildren()).toEqual([]);
128 +
129 + // Flush the promise completely
130 + await advanceTimers(50);
131 + // Renders successfully
132 + expect(ReactNoop.flush()).toEqual([
133 + 'Promise resolved [A]',
134 + 'Foo',
135 + 'Bar',
136 + 'A',
137 + 'B',
138 + ]);
139 + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
140 + });
141 +
142 + it('continues rendering siblings after suspending', async () => {
143 + ReactNoop.render(
144 + <Fallback>
145 + <Text text="A" />
146 + <AsyncText text="B" />
147 + <Text text="C" />
148 + <Text text="D" />
149 + </Fallback>,
150 + );
151 + // B suspends. Continue rendering the remaining siblings.
152 + expect(ReactNoop.flush()).toEqual(['A', 'Suspend! [B]', 'C', 'D']);
153 + // Did not commit yet.
154 + expect(ReactNoop.getChildren()).toEqual([]);
155 +
156 + // Wait for data to resolve
157 + await advanceTimers(100);
158 + // Renders successfully
159 + expect(ReactNoop.flush()).toEqual([
160 + 'Promise resolved [B]',
161 + 'A',
162 + 'B',
163 + 'C',
164 + 'D',
165 + ]);
166 + expect(ReactNoop.getChildren()).toEqual([
167 + span('A'),
168 + span('B'),
169 + span('C'),
170 + span('D'),
171 + ]);
172 + });
173 +
174 + it('retries on error', async () => {
175 + class ErrorBoundary extends React.Component {
176 + state = {error: null};
177 + componentDidCatch(error) {
178 + this.setState({error});
179 + }
180 + reset() {
181 + this.setState({error: null});
182 + }
183 + render() {
184 + if (this.state.error !== null) {
185 + return <Text text={'Caught error: ' + this.state.error.message} />;
186 + }
187 + return this.props.children;
188 + }
189 + }
190 +
191 + const errorBoundary = React.createRef();
192 + function App() {
193 + return (
194 + <Fallback>
195 + <ErrorBoundary ref={errorBoundary}>
196 + <AsyncText text="Result" ms={1000} />
197 + </ErrorBoundary>
198 + </Fallback>
199 + );
200 + }
201 +
202 + ReactNoop.render(<App />);
203 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
204 + expect(ReactNoop.getChildren()).toEqual([]);
205 +
206 + textResourceShouldFail = true;
207 + ReactNoop.expire(1000);
208 + await advanceTimers(1000);
209 + textResourceShouldFail = false;
210 +
211 + expect(ReactNoop.flush()).toEqual([
212 + 'Promise rejected [Result]',
213 + 'Error! [Result]',
214 + 'Caught error: Failed to load: Result',
215 + ]);
216 + expect(ReactNoop.getChildren()).toEqual([
217 + span('Caught error: Failed to load: Result'),
218 + ]);
219 +
220 + // Reset the error boundary and cache, and try again.
221 + errorBoundary.current.reset();
222 + cache.invalidate();
223 +
224 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
225 + ReactNoop.expire(1000);
226 + await advanceTimers(1000);
227 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
228 + expect(ReactNoop.getChildren()).toEqual([span('Result')]);
229 + });
230 +
231 + it('retries on error after falling back to a placeholder', async () => {
232 + class ErrorBoundary extends React.Component {
233 + state = {error: null};
234 + componentDidCatch(error) {
235 + this.setState({error});
236 + }
237 + reset() {
238 + this.setState({error: null});
239 + }
240 + render() {
241 + if (this.state.error !== null) {
242 + return <Text text={'Caught error: ' + this.state.error.message} />;
243 + }
244 + return this.props.children;
245 + }
246 + }
247 +
248 + const errorBoundary = React.createRef();
249 + function App() {
250 + return (
251 + <Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
252 + <ErrorBoundary ref={errorBoundary}>
253 + <AsyncText text="Result" ms={3000} />
254 + </ErrorBoundary>
255 + </Fallback>
256 + );
257 + }
258 +
259 + ReactNoop.render(<App />);
260 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
261 + expect(ReactNoop.getChildren()).toEqual([]);
262 +
263 + ReactNoop.expire(2000);
264 + await advanceTimers(2000);
265 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
266 + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
267 +
268 + textResourceShouldFail = true;
269 + ReactNoop.expire(1000);
270 + await advanceTimers(1000);
271 + textResourceShouldFail = false;
272 +
273 + expect(ReactNoop.flush()).toEqual([
274 + 'Promise rejected [Result]',
275 + 'Error! [Result]',
276 + 'Caught error: Failed to load: Result',
277 + ]);
278 + expect(ReactNoop.getChildren()).toEqual([
279 + span('Caught error: Failed to load: Result'),
280 + ]);
281 +
282 + // Reset the error boundary and cache, and try again.
283 + errorBoundary.current.reset();
284 + cache.invalidate();
285 +
286 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
287 + ReactNoop.expire(3000);
288 + await advanceTimers(3000);
289 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
290 + expect(ReactNoop.getChildren()).toEqual([span('Result')]);
291 + });
292 +
293 + it('can update at a higher priority while in a suspended state', async () => {
294 + function App(props) {
295 + return (
296 + <Fallback>
297 + <Text text={props.highPri} />
298 + <AsyncText text={props.lowPri} />
299 + </Fallback>
300 + );
301 + }
302 +
303 + // Initial mount
304 + ReactNoop.render(<App highPri="A" lowPri="1" />);
305 + ReactNoop.flush();
306 + await advanceTimers(0);
307 + ReactNoop.flush();
308 + expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
309 +
310 + // Update the low-pri text
311 + ReactNoop.render(<App highPri="A" lowPri="2" />);
312 + expect(ReactNoop.flush()).toEqual([
313 + 'A',
314 + // Suspends
315 + 'Suspend! [2]',
316 + ]);
317 +
318 + // While we're still waiting for the low-pri update to complete, update the
319 + // high-pri text at high priority.
320 + ReactNoop.flushSync(() => {
321 + ReactNoop.render(<App highPri="B" lowPri="1" />);
322 + });
323 + expect(ReactNoop.flush()).toEqual(['B', '1']);
324 + expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
325 +
326 + // Unblock the low-pri text and finish
327 + await advanceTimers(0);
328 + expect(ReactNoop.flush()).toEqual(['Promise resolved [2]']);
329 + expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
330 + });
331 +
332 + it('keeps working on lower priority work after being pinged', async () => {
333 + function App(props) {
334 + return (
335 + <Fallback>
336 + <AsyncText text="A" />
337 + {props.showB && <Text text="B" />}
338 + </Fallback>
339 + );
340 + }
341 +
342 + ReactNoop.render(<App showB={false} />);
343 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]']);
344 + expect(ReactNoop.getChildren()).toEqual([]);
345 +
346 + // Advance React's virtual time by enough to fall into a new async bucket.
347 + ReactNoop.expire(1200);
348 + ReactNoop.render(<App showB={true} />);
349 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'B']);
350 + expect(ReactNoop.getChildren()).toEqual([]);
351 +
352 + await advanceTimers(0);
353 + expect(ReactNoop.flush()).toEqual(['Promise resolved [A]', 'A', 'B']);
354 + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
355 + });
356 +
357 + it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
358 + function App(props) {
359 + if (props.hide) {
360 + return <Text text="(empty)" />;
361 + }
362 + return (
363 + <Fallback>
364 + <AsyncText ms={2000} text="Async" />
365 + </Fallback>
366 + );
367 + }
368 +
369 + // Schedule a high pri update and a low pri update, without rendering in
370 + // between.
371 + ReactNoop.interactiveUpdates(() => {
372 + // High pri
373 + ReactNoop.render(<App />);
374 + });
375 + // Low pri
376 + ReactNoop.render(<App hide={true} />);
377 +
378 + expect(ReactNoop.flush()).toEqual([
379 + // The first update suspends
380 + 'Suspend! [Async]',
381 + // but we have another pending update that we can work on
382 + '(empty)',
383 + ]);
384 + expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
385 + });
386 +
387 + it('coalesces all async updates when in a suspended state', async () => {
388 + ReactNoop.render(
389 + <Fallback>
390 + <AsyncText text="A" />
391 + </Fallback>,
392 + );
393 + ReactNoop.flush();
394 + await advanceTimers(0);
395 + ReactNoop.flush();
396 + expect(ReactNoop.getChildren()).toEqual([span('A')]);
397 +
398 + ReactNoop.render(
399 + <Fallback>
400 + <AsyncText text="B" ms={50} />
401 + </Fallback>,
402 + );
403 + expect(ReactNoop.flush()).toEqual(['Suspend! [B]']);
404 + expect(ReactNoop.getChildren()).toEqual([span('A')]);
405 +
406 + // Advance React's virtual time so that C falls into a new expiration bucket
407 + ReactNoop.expire(1000);
408 + ReactNoop.render(
409 + <Fallback>
410 + <AsyncText text="C" ms={100} />
411 + </Fallback>,
412 + );
413 + expect(ReactNoop.flush()).toEqual([
414 + // Tries C first, since it has a later expiration time
415 + 'Suspend! [C]',
416 + // Does not retry B, because its promise has not resolved yet.
417 + ]);
418 +
419 + expect(ReactNoop.getChildren()).toEqual([span('A')]);
420 +
421 + // Unblock B
422 + await advanceTimers(90);
423 + // Even though B's promise resolved, the view is still suspended because it
424 + // coalesced with C.
425 + expect(ReactNoop.flush()).toEqual(['Promise resolved [B]']);
426 + expect(ReactNoop.getChildren()).toEqual([span('A')]);
427 +
428 + // Unblock C
429 + await advanceTimers(50);
430 + expect(ReactNoop.flush()).toEqual(['Promise resolved [C]', 'C']);
431 + expect(ReactNoop.getChildren()).toEqual([span('C')]);
432 + });
433 +
434 + it('forces an expiration after an update times out', async () => {
435 + ReactNoop.render(
436 + <Fragment>
437 + <Fallback placeholder={<Text text="Loading..." />}>
438 + <AsyncText text="Async" ms={20000} />
439 + </Fallback>
440 + <Text text="Sync" />
441 + </Fragment>,
442 + );
443 +
444 + expect(ReactNoop.flush()).toEqual([
445 + // The async child suspends
446 + 'Suspend! [Async]',
447 + // Continue on the sibling
448 + 'Sync',
449 + ]);
450 + // The update hasn't expired yet, so we commit nothing.
451 + expect(ReactNoop.getChildren()).toEqual([]);
452 +
453 + // Advance both React's virtual time and Jest's timers by enough to expire
454 + // the update, but not by enough to flush the suspending promise.
455 + ReactNoop.expire(10000);
456 + await advanceTimers(10000);
457 + expect(ReactNoop.flushExpired()).toEqual([
458 + // Still suspended.
459 + 'Suspend! [Async]',
460 + // Now that the update has expired, we render the fallback UI
461 + 'Loading...',
462 + 'Sync',
463 + ]);
464 + expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
465 +
466 + // Once the promise resolves, we render the suspended view
467 + await advanceTimers(10000);
468 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
469 + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
470 + });
471 +
472 + it('switches to an inner fallback even if it expires later', async () => {
473 + ReactNoop.render(
474 + <Fragment>
475 + <Text text="Sync" />
476 + <Fallback timeout={1000} placeholder={<Text text="Loading outer..." />}>
477 + <AsyncText text="Outer content" ms={2000} />
478 + <Fallback
479 + timeout={3000}
480 + placeholder={<Text text="Loading inner..." />}>
481 + <AsyncText text="Inner content" ms={4000} />
482 + </Fallback>
483 + </Fallback>
484 + </Fragment>,
485 + );
486 +
487 + expect(ReactNoop.flush()).toEqual([
488 + 'Sync',
489 + // The async content suspends
490 + 'Suspend! [Outer content]',
491 + 'Suspend! [Inner content]',
492 + ]);
493 + // The update hasn't expired yet, so we commit nothing.
494 + expect(ReactNoop.getChildren()).toEqual([]);
495 +
496 + // Expire the outer timeout, but don't expire the inner one.
497 + // We should see the outer loading placeholder.
498 + ReactNoop.expire(1500);
499 + await advanceTimers(1500);
500 + expect(ReactNoop.flush()).toEqual([
501 + 'Sync',
502 + // Still suspended.
503 + 'Suspend! [Outer content]',
504 + 'Suspend! [Inner content]',
505 + // We attempt to fallback to the inner placeholder
506 + 'Loading inner...',
507 + // But the outer content is still suspended, so we need to fallback to
508 + // the outer placeholder.
509 + 'Loading outer...',
510 + ]);
511 +
512 + expect(ReactNoop.getChildren()).toEqual([
513 + span('Sync'),
514 + span('Loading outer...'),
515 + ]);
516 +
517 + // Resolve the outer content's promise
518 + ReactNoop.expire(1000);
519 + await advanceTimers(1000);
520 + expect(ReactNoop.flush()).toEqual([
521 + 'Promise resolved [Outer content]',
522 + 'Outer content',
523 + // Inner content still hasn't loaded
524 + 'Suspend! [Inner content]',
525 + 'Loading inner...',
526 + ]);
527 + // We should now see the inner fallback UI.
528 + expect(ReactNoop.getChildren()).toEqual([
529 + span('Sync'),
530 + span('Outer content'),
531 + span('Loading inner...'),
532 + ]);
533 +
534 + // Finally, flush the inner promise. We should see the complete screen.
535 + ReactNoop.expire(3000);
536 + await advanceTimers(3000);
537 + expect(ReactNoop.flush()).toEqual([
538 + 'Promise resolved [Inner content]',
539 + 'Inner content',
540 + ]);
541 + expect(ReactNoop.getChildren()).toEqual([
542 + span('Sync'),
543 + span('Outer content'),
544 + span('Inner content'),
545 + ]);
546 + });
547 +
548 + it('renders an expiration boundary synchronously', async () => {
549 + // Synchronously render a tree that suspends
550 + ReactNoop.flushSync(() =>
551 + ReactNoop.render(
552 + <Fragment>
553 + <Fallback placeholder={<Text text="Loading..." />}>
554 + <AsyncText text="Async" />
555 + </Fallback>
556 + <Text text="Sync" />
557 + </Fragment>,
558 + ),
559 + );
560 + expect(ReactNoop.clearYields()).toEqual([
561 + // The async child suspends
562 + 'Suspend! [Async]',
563 + // We immediately render the fallback UI
564 + 'Loading...',
565 + // Continue on the sibling
566 + 'Sync',
567 + ]);
568 + // The tree commits synchronously
569 + expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
570 +
571 + // Once the promise resolves, we render the suspended view
572 + await advanceTimers(0);
573 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
574 + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
575 + });
576 +
577 + it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
578 + ReactNoop.flushSync(() =>
579 + ReactNoop.render(
580 + <Fragment>
581 + <Fallback placeholder={<Text text="Loading (outer)..." />}>
582 + <Fallback placeholder={<AsyncText text="Loading (inner)..." />}>
583 + <AsyncText text="Async" />
584 + </Fallback>
585 + <Text text="Sync" />
586 + </Fallback>
587 + </Fragment>,
588 + ),
589 + );
590 + expect(ReactNoop.clearYields()).toEqual([
591 + 'Suspend! [Async]',
592 + 'Suspend! [Loading (inner)...]',
593 + 'Sync',
594 + 'Loading (outer)...',
595 + ]);
596 + // The tree commits synchronously
597 + expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
598 + });
599 +
600 + it('expires early with a `timeout` option', async () => {
601 + ReactNoop.render(
602 + <Fragment>
603 + <Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
604 + <AsyncText text="Async" ms={3000} />
605 + </Fallback>
606 + <Text text="Sync" />
607 + </Fragment>,
608 + );
609 +
610 + expect(ReactNoop.flush()).toEqual([
611 + // The async child suspends
612 + 'Suspend! [Async]',
613 + // Continue on the sibling
614 + 'Sync',
615 + ]);
616 + // The update hasn't expired yet, so we commit nothing.
617 + expect(ReactNoop.getChildren()).toEqual([]);
618 +
619 + // Advance both React's virtual time and Jest's timers by enough to trigger
620 + // the timeout, but not by enough to flush the promise or reach the true
621 + // expiration time.
622 + ReactNoop.expire(2000);
623 + await advanceTimers(2000);
624 + expect(ReactNoop.flush()).toEqual([
625 + // Still suspended.
626 + 'Suspend! [Async]',
627 + // Now that the expiration view has timed out, we render the fallback UI
628 + 'Loading...',
629 + 'Sync',
630 + ]);
631 + expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
632 +
633 + // Once the promise resolves, we render the suspended view
634 + await advanceTimers(1000);
635 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
636 + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
637 + });
638 +
639 + it('throws a helpful error when a synchronous update is suspended', () => {
640 + expect(() => {
641 + ReactNoop.flushSync(() =>
642 + ReactNoop.render(<Timeout>{() => <AsyncText text="Async" />}</Timeout>),
643 + );
644 + }).toThrow(
645 + 'A synchronous update was suspended, but no fallback UI was provided.',
646 + );
647 + });
648 +
649 + it('throws a helpful error when an expired update is suspended', async () => {
650 + ReactNoop.render(
651 + <Timeout>{() => <AsyncText text="Async" ms={20000} />}</Timeout>,
652 + );
653 + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
654 + await advanceTimers(10000);
655 + ReactNoop.expire(10000);
656 + expect(() => {
657 + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
658 + }).toThrow(
659 + 'An update was suspended for longer than the timeout, but no fallback ' +
660 + 'UI was provided.',
661 + );
662 + });
663 +
664 + it('a Timeout component correctly handles more than one suspended child', async () => {
665 + ReactNoop.render(
666 + <Fallback timeout={0}>
667 + <AsyncText text="A" ms={100} />
668 + <AsyncText text="B" ms={100} />
669 + </Fallback>,
670 + );
671 + ReactNoop.expire(10000);
672 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Suspend! [B]']);
673 + expect(ReactNoop.getChildren()).toEqual([]);
674 +
675 + await advanceTimers(100);
676 +
677 + expect(ReactNoop.flush()).toEqual([
678 + 'Promise resolved [A]',
679 + 'Promise resolved [B]',
680 + 'A',
681 + 'B',
682 + ]);
683 + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
684 + });
685 +
686 + it('can resume rendering earlier than a timeout', async () => {
687 + ReactNoop.render(
688 + <Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
689 + <AsyncText text="Async" ms={100} />
690 + </Fallback>,
691 + );
692 + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
693 + expect(ReactNoop.getChildren()).toEqual([]);
694 +
695 + // Advance time by an amount slightly smaller than what's necessary to
696 + // resolve the promise
697 + await advanceTimers(99);
698 +
699 + // Nothing has rendered yet
700 + expect(ReactNoop.flush()).toEqual([]);
701 + expect(ReactNoop.getChildren()).toEqual([]);
702 +
703 + // Resolve the promise
704 + await advanceTimers(1);
705 + // We can now resume rendering
706 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
707 + expect(ReactNoop.getChildren()).toEqual([span('Async')]);
708 + });
709 +
710 + it('starts working on an update even if its priority falls between two suspended levels', async () => {
711 + function App(props) {
712 + return (
713 + <Fallback timeout={10000}>
714 + {props.text === 'C' ? (
715 + <Text text="C" />
716 + ) : (
717 + <AsyncText text={props.text} ms={10000} />
718 + )}
719 + </Fallback>
720 + );
721 + }
722 +
723 + // Schedule an update
724 + ReactNoop.render(<App text="A" />);
725 + // The update should suspend.
726 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]']);
727 + expect(ReactNoop.getChildren()).toEqual([]);
728 +
729 + // Advance time until right before it expires. This number may need to
730 + // change if the default expiration for low priority updates is adjusted.
731 + await advanceTimers(4999);
732 + ReactNoop.expire(4999);
733 + expect(ReactNoop.flush()).toEqual([]);
734 + expect(ReactNoop.getChildren()).toEqual([]);
735 +
736 + // Schedule another low priority update.
737 + ReactNoop.render(<App text="B" />);
738 + // This update should also suspend.
739 + expect(ReactNoop.flush()).toEqual(['Suspend! [B]']);
740 + expect(ReactNoop.getChildren()).toEqual([]);
741 +
742 + // Schedule a high priority update. Its expiration time will fall between
743 + // the expiration times of the previous two updates.
744 + ReactNoop.interactiveUpdates(() => {
745 + ReactNoop.render(<App text="C" />);
746 + });
747 + expect(ReactNoop.flush()).toEqual(['C']);
748 + expect(ReactNoop.getChildren()).toEqual([span('C')]);
749 +
750 + await advanceTimers(10000);
751 + // Flush the remaining work.
752 + expect(ReactNoop.flush()).toEqual([
753 + 'Promise resolved [A]',
754 + 'Promise resolved [B]',
755 + ]);
756 + expect(ReactNoop.getChildren()).toEqual([span('C')]);
757 + });
758 +
759 + it('can hide a tree to unblock its surroundings', async () => {
760 + function App() {
761 + return (
762 + <Timeout ms={1000}>
763 + {didTimeout => (
764 + <Fragment>
765 + <div hidden={didTimeout}>
766 + <AsyncText text="Async" ms={3000} />
767 + </div>
768 + {didTimeout ? <Text text="Loading..." /> : null}
769 + </Fragment>
770 + )}
771 + </Timeout>
772 + );
773 + }
774 +
775 + ReactNoop.render(<App />);
776 + expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
777 + expect(ReactNoop.getChildren()).toEqual([]);
778 +
779 + ReactNoop.expire(2000);
780 + await advanceTimers(2000);
781 + expect(ReactNoop.flush()).toEqual([
782 + 'Suspend! [Async]',
783 + 'Loading...',
784 + 'Suspend! [Async]',
785 + ]);
786 + expect(ReactNoop.getChildren()).toEqual([div(), span('Loading...')]);
787 +
788 + ReactNoop.expire(1000);
789 + await advanceTimers(1000);
790 +
791 + expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
792 + expect(ReactNoop.getChildren()).toEqual([div(span('Async'))]);
793 + });
794 +
795 + describe('splitting a high-pri update into high and low', () => {
796 + React = require('react');
797 +
798 + class AsyncValue extends React.Component {
799 + state = {asyncValue: this.props.defaultValue};
800 + componentDidMount() {
801 + ReactNoop.deferredUpdates(() => {
802 + this.setState((state, props) => ({asyncValue: props.value}));
803 + });
804 + }
805 + componentDidUpdate() {
806 + if (this.props.value !== this.state.asyncValue) {
807 + ReactNoop.deferredUpdates(() => {
808 + this.setState((state, props) => ({asyncValue: props.value}));
809 + });
810 + }
811 + }
812 + render() {
813 + return this.props.children(this.state.asyncValue);
814 + }
815 + }
816 +
817 + it('coalesces async values when in a suspended state', async () => {
818 + function App(props) {
819 + const highPriText = props.text;
820 + return (
821 + <Fallback>
822 + <AsyncValue value={highPriText} defaultValue={null}>
823 + {lowPriText => (
824 + <Fragment>
825 + <Text text={`High-pri: ${highPriText}`} />
826 + {lowPriText && (
827 + <AsyncText text={`Low-pri: ${lowPriText}`} ms={100} />
828 + )}
829 + </Fragment>
830 + )}
831 + </AsyncValue>
832 + </Fallback>
833 + );
834 + }
835 +
836 + function renderAppSync(props) {
837 + ReactNoop.flushSync(() => ReactNoop.render(<App {...props} />));
838 + }
839 +
840 + // Initial mount
841 + renderAppSync({text: 'A'});
842 + expect(ReactNoop.flush()).toEqual([
843 + // First we render at high priority
844 + 'High-pri: A',
845 + // Then we come back later to render a low priority
846 + 'High-pri: A',
847 + // The low-pri view suspends
848 + 'Suspend! [Low-pri: A]',
849 + ]);
850 + expect(ReactNoop.getChildren()).toEqual([span('High-pri: A')]);
851 +
852 + // Partially flush the promise for 'A', not by enough to resolve it.
853 + await advanceTimers(99);
854 +
855 + // Advance React's virtual time so that the next update falls into a new
856 + // expiration bucket
857 + ReactNoop.expire(2000);
858 + // Update to B. At this point, the low-pri view still hasn't updated
859 + // to 'A'.
860 + renderAppSync({text: 'B'});
861 + expect(ReactNoop.flush()).toEqual([
862 + // First we render at high priority
863 + 'High-pri: B',
864 + // Then we come back later to render a low priority
865 + 'High-pri: B',
866 + // The low-pri view suspends
867 + 'Suspend! [Low-pri: B]',
868 + ]);
869 + expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]);
870 +
871 + // Flush the rest of the promise for 'A', without flushing the one
872 + // for 'B'.
873 + await advanceTimers(1);
874 + expect(ReactNoop.flush()).toEqual([
875 + // A is unblocked
876 + 'Promise resolved [Low-pri: A]',
877 + // But we don't try to render it, because there's a lower priority
878 + // update that is also suspended.
879 + ]);
880 + expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]);
881 +
882 + // Flush the remaining work.
883 + await advanceTimers(99);
884 + expect(ReactNoop.flush()).toEqual([
885 + // B is unblocked
886 + 'Promise resolved [Low-pri: B]',
887 + // Now we can continue rendering the async view
888 + 'High-pri: B',
889 + 'Low-pri: B',
890 + ]);
891 + expect(ReactNoop.getChildren()).toEqual([
892 + span('High-pri: B'),
893 + span('Low-pri: B'),
894 + ]);
895 + });
896 + });
897 +
898 + describe('a Delay component', () => {
899 + function Never() {
900 + // Throws a promise that resolves after some arbitrarily large
901 + // number of seconds. The idea is that this component will never
902 + // resolve. It's always wrapped by a Timeout.
903 + throw new Promise(resolve => setTimeout(() => resolve(), 10000));
904 + }
905 +
906 + function Delay({ms}) {
907 + return (
908 + <Timeout ms={ms}>
909 + {didTimeout => {
910 + if (didTimeout) {
911 + // Once ms has elapsed, render null. This allows the rest of the
912 + // tree to resume rendering.
913 + return null;
914 + }
915 + return <Never />;
916 + }}
917 + </Timeout>
918 + );
919 + }
920 +
921 + function DebouncedText({text, ms}) {
922 + return (
923 + <Fragment>
924 + <Delay ms={ms} />
925 + <Text text={text} />
926 + </Fragment>
927 + );
928 + }
929 +
930 + it('works', async () => {
931 + ReactNoop.render(<DebouncedText text="A" ms={1000} />);
932 + ReactNoop.flush();
933 + expect(ReactNoop.getChildren()).toEqual([]);
934 +
935 + await advanceTimers(800);
936 + ReactNoop.expire(800);
937 + ReactNoop.flush();
938 + expect(ReactNoop.getChildren()).toEqual([]);
939 +
940 + await advanceTimers(1000);
941 + ReactNoop.expire(1000);
942 + ReactNoop.flush();
943 + expect(ReactNoop.getChildren()).toEqual([span('A')]);
944 + });
945 + });
946 +});
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+27 -27
@@ -1,7 +1,7 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
4 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
4 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
5
6 // Mount
7 ⚛ (React Tree Reconciliation: Completed Root)
@@ -15,7 +15,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
15 ⚛ (Calling Lifecycle Methods: 1 Total)
16 ⚛ AllLifecycles.componentDidMount
17
18 -⚛ (Waiting for async callback... will force flush in 5230 ms)
18 +⚛ (Waiting for async callback... will force flush in 5250 ms)
19
20 // Update
21 ⚛ (React Tree Reconciliation: Completed Root)
@@ -31,7 +31,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
31 ⚛ (Calling Lifecycle Methods: 2 Total)
32 ⚛ AllLifecycles.componentDidUpdate
33
34 -⚛ (Waiting for async callback... will force flush in 5230 ms)
34 +⚛ (Waiting for async callback... will force flush in 5250 ms)
35
36 // Unmount
37 ⚛ (React Tree Reconciliation: Completed Root)
@@ -45,7 +45,7 @@ exports[`ReactDebugFiberPerf captures all lifecycles 1`] = `
45 `;
46
47 exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = `
48 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
48 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
49
50 // The commit phase should mention A and B just once
51 ⚛ (React Tree Reconciliation: Completed Root)
@@ -62,7 +62,7 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
62 ⚛ A.componentDidUpdate
63 ⚛ B.componentDidUpdate
64
65 -⚛ (Waiting for async callback... will force flush in 5230 ms)
65 +⚛ (Waiting for async callback... will force flush in 5250 ms)
66
67 // Because of deduplication, we don't know B was cascading,
68 // but we should still see the warning for the commit phase.
@@ -92,7 +92,7 @@ exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduc
92 `;
93
94 exports[`ReactDebugFiberPerf does not include AsyncMode, StrictMode, or Profiler components in measurements 1`] = `
95 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
95 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
96
97 // Mount
98 ⚛ (React Tree Reconciliation: Completed Root)
@@ -108,7 +108,7 @@ exports[`ReactDebugFiberPerf does not include AsyncMode, StrictMode, or Profiler
108 `;
109
110 exports[`ReactDebugFiberPerf does not include context provider or consumer in measurements 1`] = `
111 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
111 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
112
113 // Mount
114 ⚛ (React Tree Reconciliation: Completed Root)
@@ -143,7 +143,7 @@ exports[`ReactDebugFiberPerf does not schedule an extra callback if setState is
143 `;
144
145 exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = `
146 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
146 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
147
148 // Should not print a warning
149 ⚛ (React Tree Reconciliation: Completed Root)
@@ -156,7 +156,7 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
156 ⚛ (Committing Host Effects: 1 Total)
157 ⚛ (Calling Lifecycle Methods: 0 Total)
158
159 -⚛ (Waiting for async callback... will force flush in 5230 ms)
159 +⚛ (Waiting for async callback... will force flush in 5250 ms)
160
161 // Should not print a warning
162 ⚛ (React Tree Reconciliation: Completed Root)
@@ -172,7 +172,7 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
172 `;
173
174 exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
175 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
175 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
176
177 // Mount
178 ⚛ (React Tree Reconciliation: Completed Root)
@@ -184,7 +184,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
184 ⚛ (Committing Host Effects: 1 Total)
185 ⚛ (Calling Lifecycle Methods: 0 Total)
186
187 -⚛ (Waiting for async callback... will force flush in 5230 ms)
187 +⚛ (Waiting for async callback... will force flush in 5250 ms)
188
189 // Update
190 ⚛ (React Tree Reconciliation: Completed Root)
@@ -196,7 +196,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
196 ⚛ (Committing Host Effects: 2 Total)
197 ⚛ (Calling Lifecycle Methods: 2 Total)
198
199 -⚛ (Waiting for async callback... will force flush in 5230 ms)
199 +⚛ (Waiting for async callback... will force flush in 5250 ms)
200
201 // Unmount
202 ⚛ (React Tree Reconciliation: Completed Root)
@@ -209,7 +209,7 @@ exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = `
209 `;
210
211 exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
212 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
212 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
213
214 // Start mounting Parent and A
215 ⚛ (React Tree Reconciliation: Yielded)
@@ -217,14 +217,14 @@ exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = `
217 ⚛ A [mount]
218 ⚛ Child [mount]
219
220 -⚛ (Waiting for async callback... will force flush in 5230 ms)
220 +⚛ (Waiting for async callback... will force flush in 5250 ms)
221
222 // Mount B just a little (but not enough to memoize)
223 ⚛ (React Tree Reconciliation: Yielded)
224 ⚛ Parent [mount]
225 ⚛ B [mount]
226
227 -⚛ (Waiting for async callback... will force flush in 5230 ms)
227 +⚛ (Waiting for async callback... will force flush in 5250 ms)
228
229 // Complete B and Parent
230 ⚛ (React Tree Reconciliation: Completed Root)
@@ -263,7 +263,7 @@ exports[`ReactDebugFiberPerf measures deprioritized work 1`] = `
263 `;
264
265 exports[`ReactDebugFiberPerf properly displays the forwardRef component in measurements 1`] = `
266 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
266 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
267
268 // Mount
269 ⚛ (React Tree Reconciliation: Completed Root)
@@ -283,7 +283,7 @@ exports[`ReactDebugFiberPerf properly displays the forwardRef component in measu
283 `;
284
285 exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
286 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
286 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
287
288 // Stop on Baddie and restart from Boundary
289 ⚛ (React Tree Reconciliation: Yielded)
@@ -313,7 +313,7 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
313 `;
314
315 exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
316 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
316 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
317
318 // Will fatal
319 ⚛ (React Tree Reconciliation: Yielded)
@@ -327,7 +327,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
327 ⚛ (Committing Host Effects: 1 Total)
328 ⚛ (Calling Lifecycle Methods: 1 Total)
329
330 -⚛ (Waiting for async callback... will force flush in 5230 ms)
330 +⚛ (Waiting for async callback... will force flush in 5250 ms)
331
332 // Will reconcile from a clean state
333 ⚛ (React Tree Reconciliation: Completed Root)
@@ -342,7 +342,7 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
342 `;
343
344 exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
345 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
345 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
346
347 // Should include just A and B, no Parents
348 ⚛ (React Tree Reconciliation: Completed Root)
@@ -357,7 +357,7 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = `
357 `;
358
359 exports[`ReactDebugFiberPerf supports portals 1`] = `
360 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
360 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
361
362 ⚛ (React Tree Reconciliation: Completed Root)
363 ⚛ Parent [mount]
@@ -371,7 +371,7 @@ exports[`ReactDebugFiberPerf supports portals 1`] = `
371 `;
372
373 exports[`ReactDebugFiberPerf supports returns 1`] = `
374 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
374 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
375
376 ⚛ (React Tree Reconciliation: Completed Root)
377 ⚛ App [mount]
@@ -390,12 +390,12 @@ exports[`ReactDebugFiberPerf supports returns 1`] = `
390 `;
391
392 exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = `
393 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
393 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
394
395 ⚛ (React Tree Reconciliation: Yielded)
396 ⚛ Foo [mount]
397
398 -⚛ (Waiting for async callback... will force flush in 5230 ms)
398 +⚛ (Waiting for async callback... will force flush in 5250 ms)
399 ⛔ (React Tree Reconciliation: Completed Root) Warning: A top-level update interrupted the previous render
400 ⚛ Foo [mount]
401 ⚛ (Committing Changes)
@@ -413,7 +413,7 @@ exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] =
413 `;
414
415 exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
416 -"⛔ (Waiting for async callback... will force flush in 5230 ms) Warning: React was blocked by main thread
416 +"⛔ (Waiting for async callback... will force flush in 5250 ms) Warning: React was blocked by main thread
417
418 ⚛ (React Tree Reconciliation: Completed Root)
419 ⚛ Foo [mount]
@@ -426,7 +426,7 @@ exports[`ReactDebugFiberPerf warns if async work expires (starvation) 1`] = `
426 `;
427
428 exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
429 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
429 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
430
431 // Should print a warning
432 ⚛ (React Tree Reconciliation: Completed Root)
@@ -450,7 +450,7 @@ exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = `
450 `;
451
452 exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = `
453 -"⚛ (Waiting for async callback... will force flush in 5230 ms)
453 +"⚛ (Waiting for async callback... will force flush in 5250 ms)
454
455 // Rendering the first root
456 ⚛ (React Tree Reconciliation: Completed Root)
packages/react/src/React.js
+7
@@ -12,7 +12,9 @@ import {
12 REACT_FRAGMENT_TYPE,
13 REACT_PROFILER_TYPE,
14 REACT_STRICT_MODE_TYPE,
15 + REACT_TIMEOUT_TYPE,
16 } from 'shared/ReactSymbols';
17 +import {enableSuspense} from 'shared/ReactFeatureFlags';
18
19 import {Component, PureComponent} from './ReactBaseClasses';
20 import {createRef} from './ReactCreateRef';
@@ -53,6 +55,7 @@ const React = {
55 StrictMode: REACT_STRICT_MODE_TYPE,
56 unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
57 unstable_Profiler: REACT_PROFILER_TYPE,
58 + Timeout: REACT_TIMEOUT_TYPE,
59
60 createElement: __DEV__ ? createElementWithValidation : createElement,
61 cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
@@ -68,6 +71,10 @@ const React = {
71 },
72 };
73
74 +if (enableSuspense) {
75 + React.Timeout = REACT_TIMEOUT_TYPE;
76 +}
77 +
78 if (__DEV__) {
79 Object.assign(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, {
80 // These should not be included in production.
packages/shared/ReactFeatureFlags.js
+2
@@ -21,6 +21,8 @@ export const enablePersistentReconciler = false;
21 // Experimental error-boundary API that can recover from errors within a single
22 // render phase
23 export const enableGetDerivedStateFromCatch = false;
24 +// Suspense
25 +export const enableSuspense = false;
26 // Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
27 export const debugRenderPhaseSideEffects = false;
28
packages/shared/ReactSymbols.js
+3
@@ -42,6 +42,9 @@ export const REACT_ASYNC_MODE_TYPE = hasSymbol
42 export const REACT_FORWARD_REF_TYPE = hasSymbol
43 ? Symbol.for('react.forward_ref')
44 : 0xead0;
45 +export const REACT_TIMEOUT_TYPE = hasSymbol
46 + ? Symbol.for('react.timeout')
47 + : 0xead1;
48
49 const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
50 const FAUX_ITERATOR_SYMBOL = '@@iterator';
packages/shared/ReactTypeOfWork.js
+3 -1
@@ -23,7 +23,8 @@ export type TypeOfWork =
23 | 12
24 | 13
25 | 14
26 - | 15;
26 + | 15
27 + | 16;
28
29 export const IndeterminateComponent = 0; // Before we know whether it is functional or class
30 export const FunctionalComponent = 1;
@@ -41,3 +42,4 @@ export const ContextConsumer = 12;
42 export const ContextProvider = 13;
43 export const ForwardRef = 14;
44 export const Profiler = 15;
45 +export const TimeoutComponent = 16;
packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
+1
@@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 +export const enableSuspense = false;
20 export const warnAboutDeprecatedLifecycles = false;
21 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
22 export const enableProfilerTimer = __DEV__;
packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
+1
@@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 +export const enableSuspense = false;
20 export const warnAboutDeprecatedLifecycles = false;
21 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
22 export const enableProfilerTimer = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -15,6 +15,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-fb';
15 // Re-export dynamic flags from the fbsource version.
16 export const {
17 enableGetDerivedStateFromCatch,
18 + enableSuspense,
19 debugRenderPhaseSideEffects,
20 debugRenderPhaseSideEffectsForStrictMode,
21 warnAboutDeprecatedLifecycles,
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -15,6 +15,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-oss';
15 export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableGetDerivedStateFromCatch = false;
18 +export const enableSuspense = false;
19 export const enableMutatingReconciler = true;
20 export const enableNoopReconciler = false;
21 export const enablePersistentReconciler = false;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 +export const enableSuspense = false;
20 export const warnAboutDeprecatedLifecycles = false;
21 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
22 export const enableProfilerTimer = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false;
16 export const debugRenderPhaseSideEffectsForStrictMode = false;
17 export const enableUserTimingAPI = __DEV__;
18 export const enableGetDerivedStateFromCatch = false;
19 +export const enableSuspense = false;
20 export const warnAboutDeprecatedLifecycles = false;
21 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
22 export const enableMutatingReconciler = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -12,6 +12,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www';
12
13 // Re-export dynamic flags from the www version.
14 export const {
15 + enableSuspense,
16 debugRenderPhaseSideEffects,
17 debugRenderPhaseSideEffectsForStrictMode,
18 enableGetDerivedStateFromCatch,
packages/shared/isValidElementType.js
+2
@@ -15,6 +15,7 @@ import {
15 REACT_PROFILER_TYPE,
16 REACT_PROVIDER_TYPE,
17 REACT_STRICT_MODE_TYPE,
18 + REACT_TIMEOUT_TYPE,
19 } from 'shared/ReactSymbols';
20
21 export default function isValidElementType(type: mixed) {
@@ -26,6 +27,7 @@ export default function isValidElementType(type: mixed) {
27 type === REACT_ASYNC_MODE_TYPE ||
28 type === REACT_PROFILER_TYPE ||
29 type === REACT_STRICT_MODE_TYPE ||
30 + type === REACT_TIMEOUT_TYPE ||
31 (typeof type === 'object' &&
32 type !== null &&
33 (type.$$typeof === REACT_PROVIDER_TYPE ||