Permalink
Browse files

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
  • Loading branch information...
acdlite committed May 11, 2018
1 parent 42a1262 commit 6565795377d1d2c79a7708766f1af9e1a87517de
Showing with 1,561 additions and 76 deletions.
  1. +8 −0 packages/react-reconciler/src/ReactFiber.js
  2. +50 −2 packages/react-reconciler/src/ReactFiberBeginWork.js
  3. +11 −4 packages/react-reconciler/src/ReactFiberClassComponent.js
  4. +8 −0 packages/react-reconciler/src/ReactFiberCommitWork.js
  5. +3 −0 packages/react-reconciler/src/ReactFiberCompleteWork.js
  6. +6 −3 packages/react-reconciler/src/ReactFiberExpirationTime.js
  7. +196 −0 packages/react-reconciler/src/ReactFiberPendingPriority.js
  8. +2 −13 packages/react-reconciler/src/ReactFiberReconciler.js
  9. +23 −0 packages/react-reconciler/src/ReactFiberRoot.js
  10. +109 −19 packages/react-reconciler/src/ReactFiberScheduler.js
  11. +148 −7 packages/react-reconciler/src/ReactFiberUnwindWork.js
  12. +946 −0 packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
  13. +27 −27 packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
  14. +7 −0 packages/react/src/React.js
  15. +2 −0 packages/shared/ReactFeatureFlags.js
  16. +3 −0 packages/shared/ReactSymbols.js
  17. +3 −1 packages/shared/ReactTypeOfWork.js
  18. +1 −0 packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
  19. +1 −0 packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
  20. +1 −0 packages/shared/forks/ReactFeatureFlags.native-fb.js
  21. +1 −0 packages/shared/forks/ReactFeatureFlags.native-oss.js
  22. +1 −0 packages/shared/forks/ReactFeatureFlags.persistent.js
  23. +1 −0 packages/shared/forks/ReactFeatureFlags.test-renderer.js
  24. +1 −0 packages/shared/forks/ReactFeatureFlags.www.js
  25. +2 −0 packages/shared/isValidElementType.js
@@ -32,6 +32,7 @@ import {
ContextProvider,
ContextConsumer,
Profiler,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import getComponentName from 'shared/getComponentName';
@@ -47,6 +48,7 @@ import {
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_ASYNC_MODE_TYPE,
REACT_TIMEOUT_TYPE,
} from 'shared/ReactSymbols';
let hasBadMapPolyfill;
@@ -368,6 +370,12 @@ export function createFiberFromElement(
case REACT_RETURN_TYPE:
fiberTag = ReturnComponent;
break;
case REACT_TIMEOUT_TYPE:
fiberTag = TimeoutComponent;
// Suspense does not require async, but its children should be strict
// mode compatible.
mode |= StrictMode;
break;
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
@@ -36,19 +36,21 @@ import {
ContextProvider,
ContextConsumer,
Profiler,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import {
NoEffect,
PerformedWork,
Placement,
ContentReset,
Ref,
DidCapture,
Update,
Ref,
} from 'shared/ReactTypeOfSideEffect';
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
import {
enableGetDerivedStateFromCatch,
enableSuspense,
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
enableProfilerTimer,
@@ -91,8 +93,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
newContext: NewContext,
hydrationContext: HydrationContext<C, CX>,
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
computeExpirationForFiber: (
startTime: ExpirationTime,
fiber: Fiber,
) => ExpirationTime,
profilerTimer: ProfilerTimer,
recalculateCurrentTime: () => ExpirationTime,
) {
const {shouldSetTextContent, shouldDeprioritizeSubtree} = config;
@@ -132,6 +138,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
computeExpirationForFiber,
memoizeProps,
memoizeState,
recalculateCurrentTime,
);
// TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
@@ -758,6 +765,41 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return workInProgress.stateNode;
}
function updateTimeoutComponent(
current,
workInProgress,
renderExpirationTime,
) {
if (enableSuspense) {
const nextProps = workInProgress.pendingProps;
const prevProps = workInProgress.memoizedProps;
const prevDidTimeout = workInProgress.memoizedState;
// Check if we already attempted to render the normal state. If we did,
// and we timed out, render the placeholder state.
const alreadyCaptured =
(workInProgress.effectTag & DidCapture) === NoEffect;
const nextDidTimeout = !alreadyCaptured;
if (hasLegacyContextChanged()) {
// Normally we can bail out on props equality but if context has changed
// we don't do the bailout and we have to reuse existing props instead.
} else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
const render = nextProps.children;
const nextChildren = render(nextDidTimeout);
workInProgress.memoizedProps = nextProps;
workInProgress.memoizedState = nextDidTimeout;
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
} else {
return null;
}
}
function updatePortalComponent(
current,
workInProgress,
@@ -1209,6 +1251,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
// A return component is just a placeholder, we can just run through the
// next one immediately.
return null;
case TimeoutComponent:
return updateTimeoutComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,
@@ -152,9 +152,13 @@ export function applyDerivedStateFromProps(
export default function(
legacyContext: LegacyContext,
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
computeExpirationForFiber: (
currentTime: ExpirationTime,
fiber: Fiber,
) => ExpirationTime,
memoizeProps: (workInProgress: Fiber, props: any) => void,
memoizeState: (workInProgress: Fiber, state: any) => void,
recalculateCurrentTime: () => ExpirationTime,
) {
const {
cacheContext,
@@ -168,7 +172,8 @@ export default function(
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const expirationTime = computeExpirationForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.payload = payload;
@@ -184,7 +189,8 @@ export default function(
},
enqueueReplaceState(inst, payload, callback) {
const fiber = ReactInstanceMap.get(inst);
const expirationTime = computeExpirationForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ReplaceState;
@@ -202,7 +208,8 @@ export default function(
},
enqueueForceUpdate(inst, callback) {
const fiber = ReactInstanceMap.get(inst);
const expirationTime = computeExpirationForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.tag = ForceUpdate;
@@ -27,6 +27,7 @@ import {
HostPortal,
CallComponent,
Profiler,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import ReactErrorUtils from 'shared/ReactErrorUtils';
import {
@@ -314,6 +315,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
// We have no life-cycles associated with Profiler.
return;
}
case TimeoutComponent: {
// We have no life-cycles associated with Timeouts.
return;
}
default: {
invariant(
false,
@@ -836,6 +841,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
return;
}
case TimeoutComponent: {
return;
}
default: {
invariant(
false,
@@ -40,6 +40,7 @@ import {
Fragment,
Mode,
Profiler,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect';
import invariant from 'fbjs/lib/invariant';
@@ -593,6 +594,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return null;
case ForwardRef:
return null;
case TimeoutComponent:
return null;
case Fragment:
return null;
case Mode:
@@ -38,8 +38,11 @@ export function computeExpirationBucket(
expirationInMs: number,
bucketSizeMs: number,
): ExpirationTime {
return ceiling(
currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
return (
MAGIC_NUMBER_OFFSET +
ceiling(
currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
Oops, something went wrong.

0 comments on commit 6565795

Please sign in to comment.