Compare commits

...

7 Commits

Author SHA1 Message Date
Andrew Clark
94a255d577 Update bundle sizes for 16.4.0-alpha.0911da3 release 2018-02-26 18:12:59 -08:00
Andrew Clark
830ad204b8 Update error codes for 16.4.0-alpha.0911da3 release 2018-02-26 18:12:59 -08:00
Andrew Clark
54ac5f26eb Updating dependencies for react-noop-renderer 2018-02-26 18:08:28 -08:00
Andrew Clark
1512384497 Updating package versions for release 16.4.0-alpha.0911da3 2018-02-26 18:08:28 -08:00
Andrew Clark
22bb2679ec Updating yarn.lock file for 16.4.0-alpha.0911da3 release 2018-02-26 18:06:09 -08:00
Andrew Clark
0911da3f8e Coalesce like-priority updates made to the same component
Maybe it's ok to do this across components? It would make this much simpler.
2018-02-26 18:05:08 -08:00
Andrew Clark
9a5f3753ba [WIP] This will all make sense, soon 2018-02-26 17:52:18 -08:00
33 changed files with 2274 additions and 303 deletions

View File

@@ -1,6 +1,6 @@
{
"private": true,
"version": "16.3.0-alpha.1",
"version": "16.4.0-alpha.0911da3",
"workspaces": [
"packages/*"
],

View File

@@ -1,7 +1,7 @@
{
"name": "react-art",
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
"version": "16.3.0-alpha.1",
"version": "16.4.0-alpha.0911da3",
"main": "index.js",
"repository": "facebook/react",
"keywords": [
@@ -26,7 +26,7 @@
"prop-types": "^15.6.0"
},
"peerDependencies": {
"react": "^16.0.0 || 16.3.0-alpha.1"
"react": "^16.0.0 || 16.4.0-alpha.0911da3"
},
"files": [
"LICENSE",

View File

@@ -1,7 +1,7 @@
{
"name": "react-call-return",
"description": "Experimental APIs for multi-pass rendering in React.",
"version": "0.6.0-alpha.1",
"version": "0.6.0-alpha.0911da3",
"repository": "facebook/react",
"files": [
"LICENSE",
@@ -14,6 +14,6 @@
"object-assign": "^4.1.1"
},
"peerDependencies": {
"react": "^16.0.0 || 16.3.0-alpha.1"
"react": "^16.0.0 || 16.4.0-alpha.0911da3"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-dom",
"version": "16.3.0-alpha.1",
"version": "16.4.0-alpha.0911da3",
"description": "React package for working with the DOM.",
"main": "index.js",
"repository": "facebook/react",
@@ -19,7 +19,7 @@
"prop-types": "^15.6.0"
},
"peerDependencies": {
"react": "^16.0.0 || 16.3.0-alpha.1"
"react": "^16.0.0 || 16.4.0-alpha.0911da3"
},
"files": [
"LICENSE",

View File

@@ -1,6 +1,6 @@
{
"name": "react-is",
"version": "16.3.0-alpha.1",
"version": "16.4.0-alpha.0911da3",
"description": "Brand checking of React Elements.",
"main": "index.js",
"repository": "facebook/react",
@@ -13,7 +13,7 @@
},
"homepage": "https://reactjs.org/",
"peerDependencies": {
"react": "^16.0.0 || 16.3.0-alpha.1"
"react": "^16.0.0 || 16.4.0-alpha.0911da3"
},
"files": [
"LICENSE",

View File

@@ -11,7 +11,7 @@
"object-assign": "^4.1.1",
"prop-types": "^15.6.0",
"regenerator-runtime": "^0.11.0",
"react-reconciler": "* || 0.8.0-alpha.1"
"react-reconciler": "* || 0.8.0-alpha.0911da3"
},
"peerDependencies": {
"react": "^16.0.0"

View File

@@ -1,7 +1,7 @@
{
"name": "react-reconciler",
"description": "React package for creating custom renderers.",
"version": "0.8.0-alpha.1",
"version": "0.8.0-alpha.0911da3",
"keywords": [
"react"
],
@@ -22,7 +22,7 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^16.0.0 || 16.3.0-alpha.1"
"react": "^16.0.0 || 16.4.0-alpha.0911da3"
},
"dependencies": {
"fbjs": "^0.8.16",

View File

@@ -29,6 +29,8 @@ import {
Mode,
ContextProvider,
ContextConsumer,
LoadingComponent,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import getComponentName from 'shared/getComponentName';
@@ -42,6 +44,8 @@ import {
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_ASYNC_MODE_TYPE,
REACT_LOADING_TYPE,
REACT_TIMEOUT_TYPE,
} from 'shared/ReactSymbols';
let hasBadMapPolyfill;
@@ -347,6 +351,12 @@ export function createFiberFromElement(
case REACT_RETURN_TYPE:
fiberTag = ReturnComponent;
break;
case REACT_LOADING_TYPE:
fiberTag = LoadingComponent;
break;
case REACT_TIMEOUT_TYPE:
fiberTag = TimeoutComponent;
break;
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {

View File

@@ -14,6 +14,7 @@ import type {HostContext} from './ReactFiberHostContext';
import type {HydrationContext} from './ReactFiberHydrationContext';
import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {PriorityLevel} from './ReactPriorityLevel';
import {
IndeterminateComponent,
@@ -30,11 +31,15 @@ import {
Mode,
ContextProvider,
ContextConsumer,
LoadingComponent,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import {
NoEffect,
PerformedWork,
Placement,
ContentReset,
DidCapture,
Ref,
} from 'shared/ReactTypeOfSideEffect';
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
@@ -83,8 +88,17 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
config: HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL>,
hostContext: HostContext<C, CX>,
hydrationContext: HydrationContext<C, CX>,
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
scheduleWork: (
fiber: Fiber,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) => void,
computeUpdatePriorityForFiber: (fiber: Fiber) => PriorityLevel,
recalculateCurrentTime: () => ExpirationTime,
computeExpirationTimeForPriority: (
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
) => ExpirationTime,
) {
const {shouldSetTextContent, shouldDeprioritizeSubtree} = config;
@@ -105,9 +119,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
updateClassInstance,
} = ReactFiberClassComponent(
scheduleWork,
computeExpirationForFiber,
computeUpdatePriorityForFiber,
memoizeProps,
memoizeState,
recalculateCurrentTime,
computeExpirationTimeForPriority,
);
// TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
@@ -716,6 +732,98 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return workInProgress.stateNode;
}
function updateLoadingComponent(
current,
workInProgress,
renderExpirationTime,
) {
const nextProps = workInProgress.pendingProps;
const prevProps = workInProgress.memoizedProps;
let nextState = workInProgress.memoizedState;
if (nextState === null) {
nextState = workInProgress.memoizedState = false;
}
const prevState = current === null ? nextState : current.memoizedState;
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
nextState = workInProgress.memoizedState = processUpdateQueue(
current,
workInProgress,
updateQueue,
null,
nextProps,
renderExpirationTime,
);
}
const isLoading = nextState;
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 (prevProps === nextProps && prevState === nextState) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
const render = nextProps.children;
const nextChildren = render(isLoading);
workInProgress.memoizedProps = nextProps;
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
}
function updateTimeoutComponent(
current,
workInProgress,
renderExpirationTime,
) {
const nextProps = workInProgress.pendingProps;
const prevProps = workInProgress.memoizedProps;
let nextState = workInProgress.memoizedState;
if (nextState === null) {
nextState = workInProgress.memoizedState = false;
}
const prevState = current === null ? nextState : current.memoizedState;
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
nextState = workInProgress.memoizedState = processUpdateQueue(
current,
workInProgress,
updateQueue,
null,
null,
renderExpirationTime,
);
}
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 (
// Don't bail out if this is a restart
(workInProgress.effectTag & DidCapture) === NoEffect &&
prevProps === nextProps &&
prevState === nextState
) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
nextState = workInProgress.memoizedState = true;
}
const isExpired = nextState;
const render = nextProps.children;
const nextChildren = render(isExpired);
workInProgress.memoizedProps = nextProps;
workInProgress.memoizedState = nextState;
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
}
function updatePortalComponent(
current,
workInProgress,
@@ -1092,6 +1200,18 @@ 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 LoadingComponent:
return updateLoadingComponent(
current,
workInProgress,
renderExpirationTime,
);
case TimeoutComponent:
return updateTimeoutComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,

View File

@@ -10,6 +10,7 @@
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {CapturedValue} from './ReactCapturedValue';
import type {PriorityLevel} from './ReactPriorityLevel';
import {Update} from 'shared/ReactTypeOfSideEffect';
import {
@@ -110,10 +111,19 @@ function callGetDerivedStateFromCatch(ctor: any, capturedValues: Array<mixed>) {
}
export default function(
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
scheduleWork: (
fiber: Fiber,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) => void,
computeUpdatePriorityForFiber: (fiber: Fiber) => PriorityLevel,
memoizeProps: (workInProgress: Fiber, props: any) => void,
memoizeState: (workInProgress: Fiber, state: any) => void,
recalculateCurrentTime: () => ExpirationTime,
computeExpirationTimeForPriority: (
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
) => ExpirationTime,
) {
// Class component state updater
const updater = {
@@ -124,9 +134,15 @@ export default function(
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
const expirationTime = computeExpirationForFiber(fiber);
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
priorityLevel,
partialState,
callback,
isReplace: false,
@@ -135,7 +151,7 @@ export default function(
next: null,
};
insertUpdateIntoFiber(fiber, update);
scheduleWork(fiber, expirationTime);
scheduleWork(fiber, currentTime, expirationTime);
},
enqueueReplaceState(instance, state, callback) {
const fiber = ReactInstanceMap.get(instance);
@@ -143,9 +159,15 @@ export default function(
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
const expirationTime = computeExpirationForFiber(fiber);
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
priorityLevel,
partialState: state,
callback,
isReplace: true,
@@ -154,7 +176,7 @@ export default function(
next: null,
};
insertUpdateIntoFiber(fiber, update);
scheduleWork(fiber, expirationTime);
scheduleWork(fiber, currentTime, expirationTime);
},
enqueueForceUpdate(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
@@ -162,10 +184,16 @@ export default function(
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
const expirationTime = computeExpirationForFiber(fiber);
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
partialState: null,
priorityLevel,
callback,
isReplace: false,
isForced: true,
@@ -173,7 +201,7 @@ export default function(
next: null,
};
insertUpdateIntoFiber(fiber, update);
scheduleWork(fiber, expirationTime);
scheduleWork(fiber, currentTime, expirationTime);
},
};

View File

@@ -12,6 +12,7 @@ import type {Fiber} from './ReactFiber';
import type {FiberRoot} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
import type {PriorityLevel} from './ReactPriorityLevel';
import {
enableMutatingReconciler,
@@ -25,6 +26,8 @@ import {
HostText,
HostPortal,
CallComponent,
LoadingComponent,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import ReactErrorUtils from 'shared/ReactErrorUtils';
import {Placement, Update, ContentReset} from 'shared/ReactTypeOfSideEffect';
@@ -33,6 +36,7 @@ import invariant from 'fbjs/lib/invariant';
import {commitCallbacks} from './ReactFiberUpdateQueue';
import {onCommitUnmount} from './ReactFiberDevToolsHook';
import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue';
import {logCapturedError} from './ReactFiberErrorLogger';
import getComponentName from 'shared/getComponentName';
import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
@@ -89,12 +93,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) => void,
computeExpirationForFiber: (
startTime: ExpirationTime,
fiber: Fiber,
) => ExpirationTime,
computeUpdatePriorityForFiber: (fiber: Fiber) => PriorityLevel,
markLegacyErrorBoundaryAsFailed: (instance: mixed) => void,
recalculateCurrentTime: () => ExpirationTime,
computeExpirationTimeForPriority: (
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
) => ExpirationTime,
) {
const {getPublicInstance, mutation, persistence} = config;
@@ -152,6 +157,27 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
function scheduleExpirationBoundaryRecovery(fiber) {
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
priorityLevel,
partialState: false,
callback: null,
isReplace: true,
isForced: false,
capturedValue: null,
next: null,
};
insertUpdateIntoFiber(fiber, update);
scheduleWork(fiber, currentTime, expirationTime);
}
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
@@ -226,6 +252,21 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
// We have no life-cycles associated with portals.
return;
}
case LoadingComponent: {
return;
}
case TimeoutComponent: {
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
const promises = updateQueue.capturedValues;
if (promises !== null) {
Promise.race(promises).then(() =>
scheduleExpirationBoundaryRecovery(finishedWork),
);
}
}
return;
}
default: {
invariant(
false,
@@ -784,6 +825,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
case HostRoot: {
return;
}
case LoadingComponent: {
return;
}
case TimeoutComponent: {
return;
}
default: {
invariant(
false,

View File

@@ -34,6 +34,8 @@ import {
ContextConsumer,
Fragment,
Mode,
LoadingComponent,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import {
Placement,
@@ -605,6 +607,13 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
case ReturnComponent:
// Does nothing.
return null;
case LoadingComponent:
return null;
case TimeoutComponent:
if (workInProgress.effectTag & DidCapture) {
workInProgress.effectTag |= Update;
}
return null;
case Fragment:
return null;
case Mode:

View File

@@ -0,0 +1,243 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import {NoWork} from './ReactFiberExpirationTime';
// Because we don't have a global queue of updates, we use this module to keep
// track of the pending levels of work that have yet to be flushed. You can
// think of a PendingWork object as representing a batch of work that will
// all flush at the same time. The actual updates are spread throughout the
// update queues of all the fibers in the tree, but those updates have
// priorities that correspond to a PendingWork batch.
export type PendingWork = {
// We use `expirationTime` to represent both a priority and a timeout. There's
// no inherent reason why they need to be the same, and we may split them
// in the future.
startTime: ExpirationTime,
expirationTime: ExpirationTime,
isSuspended: boolean,
shouldTryResuming: boolean,
isRenderPhaseWork: boolean,
next: PendingWork | null,
};
function insertPendingWorkAtPosition(root, work, insertAfter, insertBefore) {
work.next = insertBefore;
if (insertAfter === null) {
root.firstPendingWork = work;
} else {
insertAfter.next = work;
}
}
export function addPendingWork(
root: FiberRoot,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
): void {
let match = null;
let insertAfter = null;
let insertBefore = root.firstPendingWork;
while (insertBefore !== null) {
if (insertBefore.expirationTime >= expirationTime) {
// Retry anything with an equal or lower expiration time
insertBefore.shouldTryResuming = true;
}
if (insertBefore.expirationTime === expirationTime) {
// Found a matching bucket. But we'll keep iterating so we can set
// `shouldTryResuming` as needed.
match = insertBefore;
// Update the start time. We always measure from the most recently
// added update.
match.startTime = startTime;
}
if (match === null && insertBefore.expirationTime > expirationTime) {
// Found the insertion position
break;
}
insertAfter = insertBefore;
insertBefore = insertBefore.next;
}
if (match === null) {
const work: PendingWork = {
startTime,
expirationTime,
isSuspended: false,
shouldTryResuming: false,
isRenderPhaseWork: false,
next: null,
};
insertPendingWorkAtPosition(root, work, insertAfter, insertBefore);
}
}
export function addRenderPhasePendingWork(
root: FiberRoot,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
): void {
// Render-phase updates are treated differently because, while they
// could potentially unblock earlier pending work, we assume that they won't.
// They are also coalesced differently (see findNextExpirationTimeToWorkOn).
let insertAfter = null;
let insertBefore = root.firstPendingWork;
while (insertBefore !== null) {
if (insertBefore.expirationTime === expirationTime) {
// Found a matching bucket
return;
}
if (insertBefore.expirationTime > expirationTime) {
// Found the insertion position
break;
}
insertAfter = insertBefore;
insertBefore = insertBefore.next;
}
// No matching level found. Create a new one.
const work: PendingWork = {
startTime,
expirationTime,
isSuspended: false,
shouldTryResuming: false,
isRenderPhaseWork: true,
next: null,
};
insertPendingWorkAtPosition(root, work, insertAfter, insertBefore);
}
export function flushPendingWork(
root: FiberRoot,
currentTime: ExpirationTime,
remainingExpirationTime: ExpirationTime,
) {
// Pop all work that has higher priority than the remaining priority.
let firstUnflushedWork = root.firstPendingWork;
while (firstUnflushedWork !== null) {
if (
remainingExpirationTime !== NoWork &&
firstUnflushedWork.expirationTime >= remainingExpirationTime
) {
break;
}
firstUnflushedWork = firstUnflushedWork.next;
}
root.firstPendingWork = firstUnflushedWork;
if (firstUnflushedWork === null) {
if (remainingExpirationTime !== NoWork) {
// There was an update during the render phase that wasn't flushed.
addRenderPhasePendingWork(root, currentTime, remainingExpirationTime);
}
} else if (
remainingExpirationTime !== NoWork &&
firstUnflushedWork.expirationTime > remainingExpirationTime
) {
// There was an update during the render phase that wasn't flushed.
addRenderPhasePendingWork(root, currentTime, remainingExpirationTime);
}
}
export function suspendPendingWork(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
let work = root.firstPendingWork;
while (work !== null) {
if (work.expirationTime === expirationTime) {
work.isSuspended = true;
work.shouldTryResuming = false;
return;
}
if (work.expirationTime > expirationTime) {
return;
}
work = work.next;
}
}
export function resumePendingWork(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
// Called when a promise resolves
let work = root.firstPendingWork;
while (work !== null) {
if (work.expirationTime === expirationTime) {
work.shouldTryResuming = true;
}
if (work.expirationTime > expirationTime) {
return;
}
work = work.next;
}
}
export function findNextExpirationTimeToWorkOn(
root: FiberRoot,
): ExpirationTime {
// If there's a non-suspended interactive expiration time, return the first
// one. If everything is suspended, return the last retry time that's either
// a) a render phase update
// b) later or equal to the last suspended time
let lastSuspendedTime = NoWork;
let lastRenderPhaseTime = NoWork;
let lastRetryTime = NoWork;
let work = root.firstPendingWork;
while (work !== null) {
if (
!work.isSuspended &&
(!work.isRenderPhaseWork || lastSuspendedTime === NoWork)
) {
return work.expirationTime;
}
if (
lastSuspendedTime === NoWork ||
lastSuspendedTime < work.expirationTime
) {
lastSuspendedTime = work.expirationTime;
}
if (work.shouldTryResuming) {
if (lastRetryTime === NoWork || lastRetryTime < work.expirationTime) {
lastRetryTime = work.expirationTime;
}
if (
work.isRenderPhaseWork &&
(lastRenderPhaseTime === NoWork ||
lastRenderPhaseTime < work.expirationTime)
) {
lastRenderPhaseTime = work.expirationTime;
}
}
work = work.next;
}
// This has the effect of coalescing all async updates that occur while we're
// in a suspended state. This prevents us from rendering an intermediate state
// that is no longer valid. An example is a tab switching interface: if
// switching to a new tab is suspended, we should only switch to the last
// tab that was clicked. If the user switches to tab A and then tab B, we
// should continue suspending until B is ready.
if (lastRetryTime >= lastSuspendedTime) {
return lastRetryTime;
}
return lastRenderPhaseTime;
}
export function findStartTime(root: FiberRoot, expirationTime: ExpirationTime) {
let match = root.firstPendingWork;
while (match !== null) {
if (match.expirationTime === expirationTime) {
return match.startTime;
}
match = match.next;
}
return NoWork;
}

View File

@@ -11,6 +11,7 @@ import type {Fiber} from './ReactFiber';
import type {FiberRoot} from './ReactFiberRoot';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {PriorityLevel} from './ReactPriorityLevel';
import {
findCurrentHostFiber,
@@ -33,6 +34,7 @@ import ReactFiberScheduler from './ReactFiberScheduler';
import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue';
import ReactFiberInstrumentation from './ReactFiberInstrumentation';
import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
import {NoPriority} from './ReactPriorityLevel';
let didWarnAboutNestedUpdates;
@@ -296,7 +298,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const {
computeUniqueAsyncExpiration,
recalculateCurrentTime,
computeExpirationForFiber,
computeUpdatePriorityForFiber,
computeExpirationTimeForPriority,
scheduleWork,
requestWork,
flushRoot,
@@ -310,13 +313,37 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
flushInteractiveUpdates,
} = ReactFiberScheduler(config);
function scheduleRootUpdate(
current: Fiber,
function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
currentTime: ExpirationTime,
priorityLevel: PriorityLevel,
expirationTime: ExpirationTime,
callback: ?Function,
) {
// TODO: If this is a nested container, this won't be the root.
const current = container.current;
if (__DEV__) {
if (ReactFiberInstrumentation.debugTool) {
if (current.alternate === null) {
ReactFiberInstrumentation.debugTool.onMountContainer(container);
} else if (element === null) {
ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
} else {
ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
}
}
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
if (__DEV__) {
if (
ReactDebugCurrentFiber.phase === 'render' &&
@@ -347,6 +374,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const update = {
expirationTime,
priorityLevel,
partialState: {element},
callback,
isReplace: false,
@@ -355,50 +383,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
next: null,
};
insertUpdateIntoFiber(current, update);
scheduleWork(current, expirationTime);
scheduleWork(current, currentTime, expirationTime);
return expirationTime;
}
function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
currentTime: ExpirationTime,
expirationTime: ExpirationTime,
callback: ?Function,
) {
// TODO: If this is a nested container, this won't be the root.
const current = container.current;
if (__DEV__) {
if (ReactFiberInstrumentation.debugTool) {
if (current.alternate === null) {
ReactFiberInstrumentation.debugTool.onMountContainer(container);
} else if (element === null) {
ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
} else {
ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
}
}
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
return scheduleRootUpdate(
current,
element,
currentTime,
expirationTime,
callback,
);
}
function findHostInstance(fiber: Fiber): PI | null {
const hostFiber = findCurrentHostFiber(fiber);
if (hostFiber === null) {
@@ -423,13 +412,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
callback: ?Function,
): ExpirationTime {
const current = container.current;
const priorityLevel = computeUpdatePriorityForFiber(current);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(current);
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
currentTime,
priorityLevel,
expirationTime,
callback,
);
@@ -442,12 +436,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
expirationTime,
callback,
) {
// TODO: Rethink this API. It's only used by the createBatch() API. Need
// to revisit that implementation once suspenders are implemented.
const priorityLevel = NoPriority;
const currentTime = recalculateCurrentTime();
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
currentTime,
priorityLevel,
expirationTime,
callback,
);

View File

@@ -9,6 +9,7 @@
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {PendingWork} from './ReactFiberPendingWork';
import {createHostRootFiber} from './ReactFiber';
import {NoWork} from './ReactFiberExpirationTime';
@@ -28,6 +29,7 @@ export type FiberRoot = {
pendingChildren: any,
// The currently active root fiber. This is the mutable root of the tree.
current: Fiber,
firstPendingWork: PendingWork | null,
pendingCommitExpirationTime: ExpirationTime,
// A finished work-in-progress HostRoot that's ready to be committed.
// TODO: The reason this is separate from isReadyForCommit is because the
@@ -63,6 +65,7 @@ export function createFiberRoot(
containerInfo: containerInfo,
pendingChildren: null,
pendingCommitExpirationTime: NoWork,
firstPendingWork: null,
finishedWork: null,
context: null,
pendingContext: null,

View File

@@ -12,6 +12,7 @@ import type {Fiber} from './ReactFiber';
import type {FiberRoot, Batch} from './ReactFiberRoot';
import type {HydrationContext} from './ReactFiberHydrationContext';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {PriorityLevel} from './ReactPriorityLevel';
import ReactErrorUtils from 'shared/ReactErrorUtils';
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
@@ -55,6 +56,14 @@ import ReactFiberHostContext from './ReactFiberHostContext';
import ReactFiberHydrationContext from './ReactFiberHydrationContext';
import ReactFiberInstrumentation from './ReactFiberInstrumentation';
import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
import {
addPendingWork,
addRenderPhasePendingWork,
flushPendingWork,
findStartTime,
findNextExpirationTimeToWorkOn,
resumePendingWork,
} from './ReactFiberPendingWork';
import {
recordEffect,
recordScheduleUpdate,
@@ -72,6 +81,13 @@ import {
startCommitLifeCyclesTimer,
stopCommitLifeCyclesTimer,
} from './ReactDebugFiberPerf';
import {
NoPriority,
RenderPriority,
SyncPriority,
DeferredPriority,
InteractivePriority,
} from './ReactPriorityLevel';
import {reset} from './ReactFiberStack';
import {createWorkInProgress} from './ReactFiber';
import {onCommitRoot} from './ReactFiberDevToolsHook';
@@ -172,7 +188,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
hostContext,
hydrationContext,
scheduleWork,
computeExpirationForFiber,
computeUpdatePriorityForFiber,
recalculateCurrentTime,
computeExpirationTimeForPriority,
);
const {completeWork} = ReactFiberCompleteWork(
config,
@@ -181,8 +199,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
);
const {throwException, unwindWork} = ReactFiberUnwindWork(
hostContext,
retryOnPromiseResolution,
scheduleWork,
isAlreadyFailedLegacyErrorBoundary,
markTimeout,
);
const {
commitResetTextContent,
@@ -197,9 +217,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
config,
onCommitPhaseError,
scheduleWork,
computeExpirationForFiber,
computeUpdatePriorityForFiber,
markLegacyErrorBoundaryAsFailed,
recalculateCurrentTime,
computeExpirationTimeForPriority,
);
const {
now,
@@ -217,10 +238,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
// Used to ensure computeUniqueAsyncExpiration is monotonically increases.
let lastUniqueAsyncExpiration: number = 0;
// Represents the expiration time that incoming updates should use. (If this
// is NoWork, use the default strategy: async updates in async mode, sync
// Represents the priority that incoming updates should use. (If this is
// NoPriority, use the default strategy: async updates in async mode, sync
// updates in sync mode.)
let expirationContext: ExpirationTime = NoWork;
let priorityContext: PriorityLevel = NoPriority;
let isWorking: boolean = false;
@@ -229,6 +250,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
let nextRoot: FiberRoot | null = null;
// The time at which we're currently rendering work.
let nextRenderExpirationTime: ExpirationTime = NoWork;
let nextStartTime: ExpirationTime = NoWork;
let nextStartTimeMs: number = -1;
let nextElapsedTimeMs: number = -1;
let nextRemainingTimeMs: number = -1;
let nextEarliestTimeoutMs: number = -1;
let nextRenderIsExpired: boolean = false;
// The next fiber with an effect that we're currently committing.
let nextEffect: Fiber | null = null;
@@ -246,7 +273,22 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
let replayUnitOfWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
stashedWorkInProgressProperties = null;
replayUnitOfWork = (failedUnitOfWork: Fiber, isAsync: boolean) => {
replayUnitOfWork = (
thrownValue: mixed,
failedUnitOfWork: Fiber,
isAsync: boolean,
) => {
if (
thrownValue !== null &&
typeof thrownValue === 'object' &&
typeof thrownValue.then === 'function'
) {
// Don't replay promises. Treat everything else like an error.
// TODO: Need to figure out a different strategy if/when we add
// support for catching other types.
return;
}
// Retore the original state of the work-in-progress
Object.assign(failedUnitOfWork, stashedWorkInProgressProperties);
switch (failedUnitOfWork.tag) {
@@ -294,6 +336,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
nextRoot = null;
nextRenderExpirationTime = NoWork;
nextStartTime = NoWork;
nextStartTimeMs = -1;
nextElapsedTimeMs = -1;
nextRemainingTimeMs = -1;
nextEarliestTimeoutMs = -1;
nextRenderIsExpired = false;
nextUnitOfWork = null;
isRootReadyForCommit = false;
@@ -570,7 +618,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
}
const remainingTime = root.current.expirationTime;
flushPendingWork(root, currentTime, root.current.expirationTime);
const remainingTime = findNextExpirationTimeToWorkOn(root);
if (remainingTime === NoWork) {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
@@ -706,7 +755,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(workInProgress);
const next = unwindWork(
workInProgress,
nextElapsedTimeMs,
nextRenderIsExpired,
nextRemainingTimeMs,
nextStartTime,
nextRenderExpirationTime,
);
// Because this fiber did not complete, don't reset its expiration time.
if (workInProgress.effectTag & DidCapture) {
// Restarting an error boundary
@@ -833,6 +889,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
resetContextStack();
nextRoot = root;
nextRenderExpirationTime = expirationTime;
nextStartTime = findStartTime(nextRoot, nextRenderExpirationTime);
recalculateCurrentTime();
if (nextStartTime === NoWork) {
nextStartTime = mostRecentCurrentTime;
nextStartTimeMs = mostRecentCurrentTimeMs;
} else {
nextStartTimeMs = expirationTimeToMs(nextStartTime);
}
nextElapsedTimeMs = mostRecentCurrentTimeMs - nextStartTimeMs;
nextRemainingTimeMs =
expirationTimeToMs(nextRenderExpirationTime) - mostRecentCurrentTimeMs;
nextEarliestTimeoutMs = nextRemainingTimeMs;
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
@@ -843,6 +911,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
let didFatal = false;
nextRenderIsExpired =
!isAsync || nextRenderExpirationTime <= mostRecentCurrentTime;
startWorkLoopTimer(nextUnitOfWork);
do {
@@ -858,7 +929,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
const failedUnitOfWork = nextUnitOfWork;
replayUnitOfWork(failedUnitOfWork, isAsync);
replayUnitOfWork(thrownValue, failedUnitOfWork, isAsync);
}
const sourceFiber: Fiber = nextUnitOfWork;
@@ -869,7 +940,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
onUncaughtError(thrownValue);
break;
}
throwException(returnFiber, sourceFiber, thrownValue);
throwException(
returnFiber,
sourceFiber,
thrownValue,
nextRenderIsExpired,
nextRemainingTimeMs,
nextElapsedTimeMs,
nextStartTime,
nextRenderExpirationTime,
);
nextUnitOfWork = completeUnitOfWork(sourceFiber);
}
break;
@@ -894,10 +974,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
} else {
// The root did not complete.
invariant(
false,
!nextRenderIsExpired,
'Expired work should have completed. This error is likely caused ' +
'by a bug in React. Please file an issue.',
);
if (nextEarliestTimeoutMs >= 0) {
const ms =
nextStartTimeMs + nextEarliestTimeoutMs - mostRecentCurrentTimeMs;
waitForTimeout(root, ms, expirationTime);
}
const firstUnblockedExpirationTime = findNextExpirationTimeToWorkOn(
root,
);
onBlock(firstUnblockedExpirationTime);
return null;
}
} else {
// There's more work to do, but we ran out of time. Yield back to
@@ -906,11 +996,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
function scheduleCapture(sourceFiber, boundaryFiber, value, expirationTime) {
function scheduleCapture(
sourceFiber,
boundaryFiber,
value,
priorityLevel,
startTime,
expirationTime,
) {
// TODO: We only support dispatching errors.
const capturedValue = createCapturedValue(value, sourceFiber);
const update = {
expirationTime,
priorityLevel,
partialState: null,
callback: null,
isReplace: false,
@@ -919,12 +1017,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
next: null,
};
insertUpdateIntoFiber(boundaryFiber, update);
scheduleWork(boundaryFiber, expirationTime);
scheduleWork(boundaryFiber, startTime, expirationTime);
}
function dispatch(
sourceFiber: Fiber,
value: mixed,
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) {
invariant(
@@ -945,13 +1045,27 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
(typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance))
) {
scheduleCapture(sourceFiber, fiber, value, expirationTime);
scheduleCapture(
sourceFiber,
fiber,
value,
priorityLevel,
startTime,
expirationTime,
);
return;
}
break;
// TODO: Handle async boundaries
case HostRoot:
scheduleCapture(sourceFiber, fiber, value, expirationTime);
scheduleCapture(
sourceFiber,
fiber,
value,
priorityLevel,
startTime,
expirationTime,
);
return;
}
fiber = fiber.return;
@@ -960,12 +1074,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
if (sourceFiber.tag === HostRoot) {
// Error was thrown at the root. There is no parent, so the root
// itself should capture it.
scheduleCapture(sourceFiber, sourceFiber, value, expirationTime);
scheduleCapture(
sourceFiber,
sourceFiber,
value,
priorityLevel,
startTime,
expirationTime,
);
}
}
function onCommitPhaseError(fiber: Fiber, error: mixed) {
return dispatch(fiber, error, Sync);
const startTime = recalculateCurrentTime();
return dispatch(fiber, error, SyncPriority, startTime, Sync);
}
function computeAsyncExpiration(currentTime: ExpirationTime) {
@@ -998,20 +1120,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return lastUniqueAsyncExpiration;
}
function computeExpirationForFiber(fiber: Fiber) {
let expirationTime;
if (expirationContext !== NoWork) {
// An explicit expiration context was set;
expirationTime = expirationContext;
function computeUpdatePriorityForFiber(fiber: Fiber): PriorityLevel {
let priorityLevel;
if (priorityContext !== NoPriority) {
// An explicit priority context was set;
priorityLevel = priorityContext;
} else if (isWorking) {
if (isCommitting) {
// Updates that occur during the commit phase should have sync priority
// by default.
expirationTime = Sync;
priorityLevel = SyncPriority;
} else {
// Updates during the render phase should expire at the same time as
// the work that is being rendered.
expirationTime = nextRenderExpirationTime;
priorityLevel = RenderPriority;
}
} else {
// No explicit expiration context was set, and we're not currently
@@ -1019,38 +1141,73 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
if (fiber.mode & AsyncMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update
const currentTime = recalculateCurrentTime();
expirationTime = computeInteractiveExpiration(currentTime);
priorityLevel = InteractivePriority;
} else {
// This is an async update
const currentTime = recalculateCurrentTime();
expirationTime = computeAsyncExpiration(currentTime);
priorityLevel = DeferredPriority;
}
} else {
// This is a sync update
expirationTime = Sync;
priorityLevel = SyncPriority;
}
}
if (isBatchingInteractiveUpdates) {
// This is an interactive update. Keep track of the lowest pending
// interactive expiration time. This allows us to synchronously flush
// all interactive updates when needed.
if (
lowestPendingInteractiveExpirationTime === NoWork ||
expirationTime > lowestPendingInteractiveExpirationTime
) {
lowestPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
return priorityLevel;
}
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
return scheduleWorkImpl(fiber, expirationTime, false);
function computeExpirationTimeForPriority(
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
) {
switch (priorityLevel) {
case NoPriority:
return nextRenderExpirationTime;
case SyncPriority:
return Sync;
case RenderPriority:
return nextRenderExpirationTime;
case InteractivePriority:
return computeInteractiveExpiration(startTime);
case DeferredPriority:
default:
return computeAsyncExpiration(startTime);
}
}
function retryOnPromiseResolution(
root: FiberRoot,
suspendedTime: ExpirationTime,
) {
resumePendingWork(root, suspendedTime);
const retryTime = findNextExpirationTimeToWorkOn(root);
if (retryTime !== NoWork) {
requestRetry(root, retryTime);
}
}
function markTimeout(timeoutMs: number) {
if (timeoutMs >= 0 && timeoutMs < nextEarliestTimeoutMs) {
nextEarliestTimeoutMs = timeoutMs;
}
}
function waitForTimeout(root, ms, suspendedTime) {
setTimeout(() => {
retryOnPromiseResolution(root, suspendedTime);
}, ms);
}
function scheduleWork(
fiber: Fiber,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) {
return scheduleWorkImpl(fiber, startTime, expirationTime, false);
}
function scheduleWorkImpl(
fiber: Fiber,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
isErrorRecovery: boolean,
) {
@@ -1063,6 +1220,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
if (isBatchingInteractiveUpdates) {
// This is an interactive update. Keep track of the lowest pending
// interactive expiration time. This allows us to synchronously flush
// all interactive updates when needed.
if (
lowestPendingInteractiveExpirationTime === NoWork ||
expirationTime > lowestPendingInteractiveExpirationTime
) {
lowestPendingInteractiveExpirationTime = expirationTime;
}
}
let node = fiber;
while (node !== null) {
// Walk the parent path to the root and update each node's
@@ -1093,6 +1262,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
interruptedBy = fiber;
resetContextStack();
}
if (!isWorking || isCommitting) {
addPendingWork(root, startTime, expirationTime);
} else {
// We're in the render phase.
addRenderPhasePendingWork(root, startTime, expirationTime);
}
if (nextRoot !== root || !isWorking) {
requestWork(root, expirationTime);
}
@@ -1126,13 +1301,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
function deferredUpdates<A>(fn: () => A): A {
const previousExpirationContext = expirationContext;
const currentTime = recalculateCurrentTime();
expirationContext = computeAsyncExpiration(currentTime);
const previousPriorityContext = priorityContext;
priorityContext = DeferredPriority;
try {
return fn();
} finally {
expirationContext = previousExpirationContext;
priorityContext = previousPriorityContext;
}
}
function syncUpdates<A, B, C0, D, R>(
@@ -1142,12 +1316,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
c: C0,
d: D,
): R {
const previousExpirationContext = expirationContext;
expirationContext = Sync;
const previousPriorityContext = priorityContext;
priorityContext = SyncPriority;
try {
return fn(a, b, c, d);
} finally {
expirationContext = previousExpirationContext;
priorityContext = previousPriorityContext;
}
}
@@ -1206,6 +1380,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}
function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) {
if (
root.remainingExpirationTime === NoWork ||
root.remainingExpirationTime < expirationTime
) {
// For a retry, only update the remaining expiration time if it has a
// *lower priority* than the existing value. This is because, on a retry,
// we should attempt to coalesce as much as possible.
requestWork(root, expirationTime);
}
}
// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -1564,6 +1750,16 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
function onBlock(remainingExpirationTime: ExpirationTime) {
invariant(
nextFlushedRoot !== null,
'Should be working on a root. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
// This root was blocked. Unschedule it until there's another update.
nextFlushedRoot.remainingExpirationTime = remainingExpirationTime;
}
// TODO: Batching should be implemented at the renderer level, not inside
// the reconciler.
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
@@ -1666,7 +1862,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return {
recalculateCurrentTime,
computeExpirationForFiber,
computeUpdatePriorityForFiber,
computeExpirationTimeForPriority,
scheduleWork,
requestWork,
flushRoot,

View File

@@ -7,7 +7,11 @@
*/
import {createCapturedValue} from './ReactCapturedValue';
import {ensureUpdateQueues} from './ReactFiberUpdateQueue';
import {suspendPendingWork} from './ReactFiberPendingWork';
import {
ensureUpdateQueues,
insertUpdateIntoFiber,
} from './ReactFiberUpdateQueue';
import {
ClassComponent,
@@ -15,6 +19,8 @@ import {
HostComponent,
HostPortal,
ContextProvider,
LoadingComponent,
TimeoutComponent,
} from 'shared/ReactTypeOfWork';
import {
NoEffect,
@@ -22,6 +28,8 @@ import {
Incomplete,
ShouldCapture,
} from 'shared/ReactTypeOfSideEffect';
import {Sync} from './ReactFiberExpirationTime';
import {NoPriority} from './ReactPriorityLevel';
import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags';
@@ -31,29 +39,206 @@ import {
} from './ReactFiberContext';
import {popProvider} from './ReactFiberNewContext';
import invariant from 'fbjs/lib/invariant';
const SuspendException = 1;
const SuspendAndLoadingException = 2;
function createRootExpirationError(sourceFiber, renderExpirationTime) {
try {
// TODO: Better error messages.
invariant(
renderExpirationTime !== Sync,
'A synchronous update was suspended, but no fallback UI was provided.',
);
invariant(
false,
'An update was suspended for longer than the timeout, but no fallback ' +
'UI was provided.',
);
} catch (error) {
return error;
}
}
export default function(
hostContext: HostContext<C, CX>,
retryOnPromiseResolution: (
root: FiberRoot,
blockedTime: ExpirationTime,
) => void,
scheduleWork: (
fiber: Fiber,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) => void,
isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean,
markTimeout: (timeoutMs: number) => void,
) {
const {popHostContainer, popHostContext} = hostContext;
function waitForPromise(root, promise, suspendedTime) {
promise.then(() => retryOnPromiseResolution(root, suspendedTime));
}
function scheduleLoadingState(
workInProgress,
renderStartTime,
renderExpirationTime,
) {
const slightlyEarlierExpirationTime = renderExpirationTime - 1;
const loadingUpdate = {
expirationTime: slightlyEarlierExpirationTime,
priorityLevel: NoPriority,
partialState: true,
callback: null,
isReplace: true,
isForced: false,
capturedValue: null,
next: null,
};
insertUpdateIntoFiber(workInProgress, loadingUpdate);
const revertUpdate = {
expirationTime: renderExpirationTime,
priorityLevel: NoPriority,
partialState: false,
callback: null,
isReplace: true,
isForced: false,
capturedValue: null,
next: null,
};
insertUpdateIntoFiber(workInProgress, revertUpdate);
scheduleWork(
workInProgress,
renderStartTime,
slightlyEarlierExpirationTime,
);
return false;
}
function throwException(
returnFiber: Fiber,
sourceFiber: Fiber,
rawValue: mixed,
value: mixed,
renderIsExpired: boolean,
remainingTimeMs: number,
elapsedMs: number,
renderStartTime: number,
renderExpirationTime: ExpirationTime,
) {
// The source fiber did not complete.
sourceFiber.effectTag |= Incomplete;
// Its effect list is no longer valid.
sourceFiber.firstEffect = sourceFiber.lastEffect = null;
const value = createCapturedValue(rawValue, sourceFiber);
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
// This is a thenable.
let typeOfException = SuspendAndLoadingException;
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
const root: FiberRoot = workInProgress.stateNode;
switch (typeOfException) {
case SuspendAndLoadingException:
case SuspendException: {
if (!renderIsExpired) {
// Set-up timer using render expiration time
const suspendedTime = renderExpirationTime;
const promise = value;
suspendPendingWork(root, suspendedTime);
waitForPromise(root, promise, suspendedTime);
return;
}
// The root expired, but no fallback was provided. Throw a
// helpful error.
value = createRootExpirationError(
sourceFiber,
renderExpirationTime,
);
break;
}
}
break;
}
case TimeoutComponent:
switch (typeOfException) {
case SuspendAndLoadingException:
case SuspendException: {
const didExpire = workInProgress.memoizedState;
const timeout = workInProgress.pendingProps.ms;
// Check if the boundary should capture promises that threw.
let shouldCapture;
if (workInProgress.effectTag & DidCapture) {
// Already captured during this render. Can't capture again.
shouldCapture = false;
} else if (didExpire || renderIsExpired) {
// Render is expired.
shouldCapture = true;
} else if (
typeof timeout === 'number' &&
elapsedMs >= timeout
) {
// The elapsed time exceeds the provided timeout.
shouldCapture = true;
} else {
// There's still time left. Bubble to the next boundary.
shouldCapture = false;
}
if (shouldCapture) {
workInProgress.effectTag |= ShouldCapture;
ensureUpdateQueues(workInProgress);
const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
const capturedValues = updateQueue.capturedValues;
if (capturedValues === null) {
updateQueue.capturedValues = [value];
} else {
capturedValues.push(value);
}
return workInProgress;
} else {
if (typeof timeout === 'number') {
markTimeout(timeout);
}
}
}
}
break;
case LoadingComponent:
switch (typeOfException) {
case SuspendAndLoadingException: {
const current = workInProgress.alternate;
const isLoading = workInProgress.memoizedState;
if (current !== null && !isLoading && !renderIsExpired) {
// Schedule loading update
scheduleLoadingState(
workInProgress,
renderStartTime,
renderExpirationTime,
);
typeOfException = SuspendException;
break;
}
}
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
// We didn't find a boundary that could handle this type of exception. Start
// over and traverse parent path again, this time treating the exception
// as an error.
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
@@ -97,7 +282,14 @@ export default function(
} while (workInProgress !== null);
}
function unwindWork(workInProgress) {
function unwindWork(
workInProgress,
elapsedMs,
renderIsExpired,
remainingTimeMs,
renderStartTime,
renderExpirationTime,
) {
switch (workInProgress.tag) {
case ClassComponent: {
popLegacyContextProvider(workInProgress);
@@ -122,6 +314,14 @@ export default function(
popHostContext(workInProgress);
return null;
}
case TimeoutComponent: {
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
case HostPortal:
popHostContainer(workInProgress);
return null;

View File

@@ -9,6 +9,7 @@
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {PriorityLevel} from './ReactPriorityLevel';
import type {CapturedValue} from './ReactCapturedValue';
import {
@@ -16,12 +17,17 @@ import {
debugRenderPhaseSideEffectsForStrictMode,
} from 'shared/ReactFeatureFlags';
import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect';
import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork';
import {
ClassComponent,
HostRoot,
LoadingComponent,
} from 'shared/ReactTypeOfWork';
import invariant from 'fbjs/lib/invariant';
import warning from 'fbjs/lib/warning';
import {StrictMode} from './ReactTypeOfMode';
import {NoWork} from './ReactFiberExpirationTime';
import {NoPriority} from './ReactPriorityLevel';
let didWarnUpdateInsideUpdate;
@@ -38,6 +44,7 @@ type Callback = mixed;
export type Update<State> = {
expirationTime: ExpirationTime,
priorityLevel: PriorityLevel,
partialState: PartialState<any, any>,
callback: Callback | null,
isReplace: boolean,
@@ -97,6 +104,27 @@ export function insertUpdateIntoQueue<State>(
queue: UpdateQueue<State>,
update: Update<State>,
): void {
const priorityLevel = update.priorityLevel;
if (
priorityLevel !== NoPriority &&
queue.expirationTime <= update.expirationTime
) {
let node = queue.first;
let latestExpirationTimeWithMatchingPriority = NoWork;
while (node !== null) {
if (
node.priorityLevel === priorityLevel &&
node.expirationTime > latestExpirationTimeWithMatchingPriority
) {
latestExpirationTimeWithMatchingPriority = node.expirationTime;
}
node = node.next;
}
if (latestExpirationTimeWithMatchingPriority !== NoWork) {
update.expirationTime = latestExpirationTimeWithMatchingPriority;
}
}
// Append the update to the end of the list.
if (queue.last === null) {
// Queue is empty
@@ -194,6 +222,7 @@ export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
switch (fiber.tag) {
case HostRoot:
case ClassComponent:
case LoadingComponent:
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
return NoWork;

View File

@@ -0,0 +1,16 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type PriorityLevel = 0 | 1 | 2 | 3 | 4;
export const NoPriority = 0;
export const RenderPriority = 1;
export const SyncPriority = 2;
export const InteractivePriority = 3;
export const DeferredPriority = 4;

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment node
*/
'use strict';
let React;
let Fragment;
let ReactNoop;
let ReactFeatureFlags;
describe('ReactExpiration', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
Fragment = React.Fragment;
ReactNoop = require('react-noop-renderer');
});
function span(prop) {
return {type: 'span', children: [], prop};
}
it('increases priority of updates as time progresses', () => {
ReactNoop.render(<span prop="done" />);
expect(ReactNoop.getChildren()).toEqual([]);
// Nothing has expired yet because time hasn't advanced.
ReactNoop.flushExpired();
expect(ReactNoop.getChildren()).toEqual([]);
// Advance time a bit, but not enough to expire the low pri update.
ReactNoop.expire(4500);
ReactNoop.flushExpired();
expect(ReactNoop.getChildren()).toEqual([]);
// Advance by another second. Now the update should expire and flush.
ReactNoop.expire(1000);
ReactNoop.flushExpired();
expect(ReactNoop.getChildren()).toEqual([span('done')]);
});
it('coalesces updates to the same component', () => {
class Foo extends React.Component {
state = {step: 0};
componentDidUpdate() {
ReactNoop.yield(`Did update ${this.props.label}: ${this.state.step}`);
}
render() {
ReactNoop.yield(`Render ${this.props.label}: ${this.state.step}`);
return <span prop={`${this.props.label}: ${this.state.step}`} />;
}
}
let a = React.createRef();
let b = React.createRef();
ReactNoop.render(
<Fragment>
<Foo ref={a} label="A" />
<Foo ref={b} label="B" />
</Fragment>,
);
ReactNoop.flush();
a.value.setState({step: 1});
// Advance time to move into a new expiration bucket
ReactNoop.expire(2000);
// Update A again. This update should coalesce with the previous update.
a.value.setState({step: 2});
// Update B. This is the first update, so it has nothing to coalesce with.
b.value.setState({step: 2});
// Advance time by enough to expire step 1, but not step 2.
ReactNoop.expire(4000);
expect(ReactNoop.flushExpired()).toEqual([
// Even though we called setState on A twice, both updates should flush in
// a single batch.
'Render A: 2',
'Did update A: 2',
// Update B has not expired yet, even though its setState was scheduled
// at the same time as A
]);
expect(ReactNoop.getChildren()).toEqual([span('A: 2'), span('B: 0')]);
// Now expire B, too.
ReactNoop.expire(2000);
expect(ReactNoop.flushExpired()).toEqual([
'Render B: 2',
'Did update B: 2',
]);
expect(ReactNoop.getChildren()).toEqual([span('A: 2'), span('B: 2')]);
});
});

View File

@@ -1,45 +0,0 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment node
*/
'use strict';
let React;
let ReactNoop;
describe('ReactExpiration', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
});
function span(prop) {
return {type: 'span', children: [], prop};
}
it('increases priority of updates as time progresses', () => {
ReactNoop.render(<span prop="done" />);
expect(ReactNoop.getChildren()).toEqual([]);
// Nothing has expired yet because time hasn't advanced.
ReactNoop.flushExpired();
expect(ReactNoop.getChildren()).toEqual([]);
// Advance time a bit, but not enough to expire the low pri update.
ReactNoop.expire(4500);
ReactNoop.flushExpired();
expect(ReactNoop.getChildren()).toEqual([]);
// Advance by another second. Now the update should expire and flush.
ReactNoop.expire(1000);
ReactNoop.flushExpired();
expect(ReactNoop.getChildren()).toEqual([span('done')]);
});
});

View File

@@ -0,0 +1,976 @@
let React;
let Fragment;
let ReactNoop;
let SimpleCacheProvider;
let Loading;
let Timeout;
let cache;
let readText;
describe('ReactSuspense', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
Fragment = React.Fragment;
ReactNoop = require('react-noop-renderer');
SimpleCacheProvider = require('simple-cache-provider');
Loading = React.Loading;
Timeout = React.Timeout;
cache = SimpleCacheProvider.createCache();
readText = SimpleCacheProvider.createResource(([text, ms = 0]) => {
return new Promise(resolve =>
setTimeout(() => {
ReactNoop.yield(`Promise resolved [${text}]`);
resolve(text);
}, ms),
);
}, ([text, ms]) => text);
});
function div(...children) {
children = children.map(c => (typeof c === 'string' ? {text: c} : c));
return {type: 'div', children, prop: undefined};
}
function span(prop) {
return {type: 'span', children: [], prop};
}
function advanceTimers(ms) {
// Note: This advances Jest's virtual time but not React's. Use
// ReactNoop.expire for that.
if (typeof ms !== 'number') {
throw new Error('Must specify ms');
}
jest.advanceTimersByTime(ms);
// Wait until the end of the current tick
return new Promise(resolve => {
setImmediate(resolve);
});
}
function Text(props) {
ReactNoop.yield(props.text);
return <span prop={props.text} />;
}
function AsyncText(props) {
const text = props.text;
try {
readText(cache, [props.text, props.ms]);
ReactNoop.yield(text);
return <span prop={text} />;
} catch (promise) {
ReactNoop.yield(`Suspend! [${text}]`);
throw promise;
}
}
function Fallback(props) {
return (
<Timeout ms={props.timeout}>
{didExpire => (didExpire ? props.placeholder : props.children)}
</Timeout>
);
}
it('suspends rendering and continues later', async () => {
function Bar(props) {
ReactNoop.yield('Bar');
return props.children;
}
function Foo() {
ReactNoop.yield('Foo');
return (
<Bar>
<AsyncText text="A" ms={100} />
<Text text="B" />
</Bar>
);
}
ReactNoop.render(<Foo />);
expect(ReactNoop.flush()).toEqual([
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
// But we keep rendering the siblings
'B',
]);
expect(ReactNoop.getChildren()).toEqual([]);
// Flush some of the time
await advanceTimers(50);
// Still nothing...
expect(ReactNoop.flush()).toEqual([]);
expect(ReactNoop.getChildren()).toEqual([]);
// Flush the promise completely
await advanceTimers(50);
// Renders successfully
expect(ReactNoop.flush()).toEqual([
'Promise resolved [A]',
'Foo',
'Bar',
'A',
'B',
]);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
it('continues rendering siblings after suspending', async () => {
ReactNoop.render(
<Fragment>
<Text text="A" />
<AsyncText text="B" />
<Text text="C" />
<Text text="D" />
</Fragment>,
);
// B suspends. Continue rendering the remaining siblings.
expect(ReactNoop.flush()).toEqual(['A', 'Suspend! [B]', 'C', 'D']);
// Did not commit yet.
expect(ReactNoop.getChildren()).toEqual([]);
// Wait for data to resolve
await advanceTimers(100);
// Renders successfully
expect(ReactNoop.flush()).toEqual([
'Promise resolved [B]',
'A',
'B',
'C',
'D',
]);
expect(ReactNoop.getChildren()).toEqual([
span('A'),
span('B'),
span('C'),
span('D'),
]);
});
it('can render an alternate view at a higher priority', async () => {
function App(props) {
return (
<Loading>
{isLoading => (
<Fragment>
{isLoading ? <Text text="Loading..." /> : null}
<Text text="A" />
<Text text="B" />
<Text text="C" />
{props.step >= 1 ? <AsyncText text="D" /> : null}
</Fragment>
)}
</Loading>
);
}
ReactNoop.render(<App step={0} />);
expect(ReactNoop.flush()).toEqual(['A', 'B', 'C']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B'), span('C')]);
ReactNoop.render(<App step={1} />);
expect(ReactNoop.flush()).toEqual([
'A',
'B',
'C',
// D suspends, which triggers the loading state.
'Suspend! [D]',
'Loading...',
'A',
'B',
'C',
]);
expect(ReactNoop.getChildren()).toEqual([
span('Loading...'),
span('A'),
span('B'),
span('C'),
]);
// Wait for data to resolve
await advanceTimers(100);
expect(ReactNoop.flush()).toEqual([
'Promise resolved [D]',
'A',
'B',
'C',
'D',
]);
expect(ReactNoop.getChildren()).toEqual([
span('A'),
span('B'),
span('C'),
span('D'),
]);
});
it('can suspend inside a boundary', async () => {
function App(props) {
return (
<Loading>
{isLoading => {
if (isLoading) {
return <AsyncText text="Loading..." ms={50} />;
}
return props.step > 0 ? (
<AsyncText text="Final result" ms={100} />
) : (
<Text text="Initial text" />
);
}}
</Loading>
);
}
// Initial mount
ReactNoop.render(<App step={0} />);
expect(ReactNoop.flush()).toEqual(['Initial text']);
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
ReactNoop.render(<App step={1} />);
expect(ReactNoop.flush()).toEqual([
'Suspend! [Final result]',
'Suspend! [Loading...]',
]);
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
// Unblock the "Loading..." view
await advanceTimers(50);
expect(ReactNoop.flush()).toEqual([
// Renders the loading view,
'Promise resolved [Loading...]',
'Loading...',
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
// Unblock the rest.
await advanceTimers(50);
// Now we can render the final result.
expect(ReactNoop.flush()).toEqual([
'Promise resolved [Final result]',
'Final result',
]);
expect(ReactNoop.getChildren()).toEqual([span('Final result')]);
});
it('nested boundaries do not capture if an outer boundary is suspended', async () => {
function App(props) {
return (
<Loading>
{() => (
<Loading>
{() =>
props.text ? (
<AsyncText text={props.text} />
) : (
<Text text="Initial text" />
)
}
</Loading>
)}
</Loading>
);
}
ReactNoop.render(<App />);
expect(ReactNoop.flush()).toEqual(['Initial text']);
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
ReactNoop.render(<App text="A" />);
expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Initial text']);
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
// Move B into a later expiration bucket
ReactNoop.expire(2000);
ReactNoop.render(<App text="B" />);
expect(ReactNoop.flush()).toEqual([
// B is suspended
'Suspend! [B]',
// It doesn't bother trying to render a loading state because it
// would be based on A, which we already know is suspended.
]);
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
// Unblock
await advanceTimers(0);
expect(ReactNoop.flush()).toEqual([
'Promise resolved [A]',
'Promise resolved [B]',
'B',
]);
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
it('can suspend, resume, then resume again in a later update, with correct bubbling', async () => {
function App(props) {
return (
<Loading>
{isLoading => (
<Fragment>
{isLoading ? <Text text="Loading..." /> : null}
<AsyncText text={props.text} />
</Fragment>
)}
</Loading>
);
}
ReactNoop.render(<App text="Initial text" />);
ReactNoop.flush();
await advanceTimers(0);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
ReactNoop.render(<App text="Update" />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([
span('Loading...'),
span('Initial text'),
]);
await advanceTimers(0);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('Update')]);
ReactNoop.render(<App text="Another update" />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([
span('Loading...'),
span('Update'),
]);
await advanceTimers(0);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('Another update')]);
});
it('bubbles to next boundary if it suspends', async () => {
function App(props) {
return (
<Loading>
{isLoadingOuter => (
<Fragment>
{isLoadingOuter ? <Text text="Loading (outer)..." /> : null}
<Loading>
{isLoadingInner => (
<div>
{isLoadingInner ? (
<AsyncText text="Loading (inner)..." ms={100} />
) : null}
{props.step > 0 ? (
<AsyncText text="Final result" ms={200} />
) : (
<Text text="Initial text" />
)}
</div>
)}
</Loading>
</Fragment>
)}
</Loading>
);
}
ReactNoop.render(<App step={0} />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([div(span('Initial text'))]);
// Update to display "Final result"
ReactNoop.render(<App step={1} />);
expect(ReactNoop.flush()).toEqual([
// "Final result" suspends.
'Suspend! [Final result]',
// The inner boundary renders a loading view. The loading view also suspends.
'Suspend! [Loading (inner)...]',
// (Continues rendering siblings even though something suspended)
'Initial text',
// Bubble up and retry at the next boundary. This time it's successful.
'Loading (outer)...',
'Initial text',
]);
expect(ReactNoop.getChildren()).toEqual([
span('Loading (outer)...'),
div(span('Initial text')),
]);
// Unblock the inner boundary.
await advanceTimers(100);
expect(ReactNoop.flush()).toEqual([
'Promise resolved [Loading (inner)...]',
// Now the inner loading view should display, not the outer one.
'Loading (inner)...',
'Initial text',
]);
expect(ReactNoop.getChildren()).toEqual([
div(span('Loading (inner)...'), span('Initial text')),
]);
// Flush all the promises.
await advanceTimers(100);
// Now the final result should display, with no loading state.
expect(ReactNoop.flush()).toEqual([
'Promise resolved [Final result]',
'Final result',
]);
expect(ReactNoop.getChildren()).toEqual([div(span('Final result'))]);
});
it('does not bubble through a boundary unless that boundary already captured', () => {
function App(props) {
return (
<Loading>
{isLoading => (
<Fragment>
<Loading>
{spinnerIsLoading => (
<Fragment>
{spinnerIsLoading && <Text text="(fallback spinner)" />}
{isLoading && <AsyncText text="(spinner)" />}
</Fragment>
)}
</Loading>
<Text text="Initial text" />
{props.step > 0 && <AsyncText text="More" />}
</Fragment>
)}
</Loading>
);
}
ReactNoop.render(<App step={0} />);
expect(ReactNoop.flush()).toEqual(['Initial text']);
expect(ReactNoop.getChildren()).toEqual([span('Initial text')]);
ReactNoop.render(<App step={1} />);
expect(ReactNoop.flush()).toEqual([
'Initial text',
'Suspend! [More]',
'Suspend! [(spinner)]',
'Initial text',
'(fallback spinner)',
]);
});
it('can resume a lower priority update', () => {
function App(props) {
return (
<Loading>
{isLoading => (
<Fragment>
{isLoading ? <Text text="Loading..." /> : null}
{props.showContent ? (
<AsyncText text="Content" />
) : (
<Text text="(empty)" />
)}
</Fragment>
)}
</Loading>
);
}
// Mount the initial view
ReactNoop.render(<App showContent={false} />);
expect(ReactNoop.flush()).toEqual(['(empty)']);
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
// Toggle to show the content, which is async
ReactNoop.render(<App showContent={true} />);
expect(ReactNoop.flush()).toEqual([
// The content suspends because it's async
'Suspend! [Content]',
// Show the loading view
'Loading...',
'(empty)',
]);
});
it('can update at a higher priority while in a suspended state', async () => {
function App(props) {
return (
<Fragment>
<Text text={props.highPri} />
<AsyncText text={props.lowPri} />
</Fragment>
);
}
// Initial mount
ReactNoop.render(<App highPri="A" lowPri="1" />);
ReactNoop.flush();
await advanceTimers(0);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
// Update the low-pri text
ReactNoop.render(<App highPri="A" lowPri="2" />);
expect(ReactNoop.flush()).toEqual([
'A',
// Suspends
'Suspend! [2]',
]);
// While we're still waiting for the low-pri update to complete, update the
// high-pri text at high priority.
ReactNoop.flushSync(() => {
ReactNoop.render(<App highPri="B" lowPri="1" />);
});
expect(ReactNoop.flush()).toEqual(['B', '1']);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
// Unblock the low-pri text and finish
await advanceTimers(0);
expect(ReactNoop.flush()).toEqual(['Promise resolved [2]']);
expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
});
it('keeps working on lower priority work after being unblocked', async () => {
function App(props) {
return (
<Fragment>
<AsyncText text="A" />
{props.showB && <Text text="B" />}
</Fragment>
);
}
ReactNoop.render(<App showB={false} />);
expect(ReactNoop.flush()).toEqual(['Suspend! [A]']);
expect(ReactNoop.getChildren()).toEqual([]);
// Advance React's virtual time by enough to fall into a new async bucket.
ReactNoop.expire(1200);
ReactNoop.render(<App showB={true} />);
expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'B']);
expect(ReactNoop.getChildren()).toEqual([]);
await advanceTimers(0);
expect(ReactNoop.flush()).toEqual(['Promise resolved [A]', 'A', 'B']);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
it('coalesces all async updates when in a suspended state', async () => {
ReactNoop.render(<AsyncText text="A" />);
ReactNoop.flush();
await advanceTimers(0);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('A')]);
ReactNoop.render(<AsyncText text="B" ms={50} />);
expect(ReactNoop.flush()).toEqual(['Suspend! [B]']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
// Advance React's virtual time so that C falls into a new expiration bucket
ReactNoop.expire(1000);
ReactNoop.render(<AsyncText text="C" ms={100} />);
expect(ReactNoop.flush()).toEqual([
// Tries C first, since it has a later expiration time
'Suspend! [C]',
// Does not retry B, because its promise has not resolved yet.
]);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
// Unblock B
await advanceTimers(90);
// Even though B's promise resolved, the view is still suspended because it
// coalesced with C.
expect(ReactNoop.flush()).toEqual(['Promise resolved [B]']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);
// Unblock C
await advanceTimers(50);
expect(ReactNoop.flush()).toEqual(['Promise resolved [C]', 'C']);
expect(ReactNoop.getChildren()).toEqual([span('C')]);
});
it('forces an expiration after an update times out', async () => {
ReactNoop.render(
<Fragment>
<Fallback placeholder={<Text text="Loading..." />}>
<AsyncText text="Async" ms={20000} />
</Fallback>
<Text text="Sync" />
</Fragment>,
);
expect(ReactNoop.flush()).toEqual([
// The async child suspends
'Suspend! [Async]',
// Continue on the sibling
'Sync',
]);
// The update hasn't expired yet, so we commit nothing.
expect(ReactNoop.getChildren()).toEqual([]);
// Advance both React's virtual time and Jest's timers by enough to expire
// the update, but not by enough to flush the suspending promise.
ReactNoop.expire(10000);
await advanceTimers(10000);
expect(ReactNoop.flushExpired()).toEqual([
// Still suspended.
'Suspend! [Async]',
// Now that the update has expired, we render the fallback UI
'Loading...',
'Sync',
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
// Once the promise resolves, we render the suspended view
await advanceTimers(10000);
expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
it('renders an expiration boundary synchronously', async () => {
// Synchronously render a tree that suspends
ReactNoop.flushSync(() =>
ReactNoop.render(
<Fragment>
<Fallback placeholder={<Text text="Loading..." />}>
<AsyncText text="Async" />
</Fallback>
<Text text="Sync" />
</Fragment>,
),
);
expect(ReactNoop.clearYields()).toEqual([
// The async child suspends
'Suspend! [Async]',
// We immediately render the fallback UI
'Loading...',
// Continue on the sibling
'Sync',
]);
// The tree commits synchronously
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
// Once the promise resolves, we render the suspended view
await advanceTimers(0);
expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
ReactNoop.flushSync(() =>
ReactNoop.render(
<Fragment>
<Fallback placeholder={<Text text="Loading (outer)..." />}>
<Fallback placeholder={<AsyncText text="Loading (inner)..." />}>
<AsyncText text="Async" />
</Fallback>
<Text text="Sync" />
</Fallback>
</Fragment>,
),
);
expect(ReactNoop.clearYields()).toEqual([
'Suspend! [Async]',
'Suspend! [Loading (inner)...]',
'Sync',
'Loading (outer)...',
]);
// The tree commits synchronously
expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
});
it('expires early with a `timeout` option', async () => {
ReactNoop.render(
<Fragment>
<Fallback timeout={100} placeholder={<Text text="Loading..." />}>
<AsyncText text="Async" ms={1000} />
</Fallback>
<Text text="Sync" />
</Fragment>,
);
expect(ReactNoop.flush()).toEqual([
// The async child suspends
'Suspend! [Async]',
// Continue on the sibling
'Sync',
]);
// The update hasn't expired yet, so we commit nothing.
expect(ReactNoop.getChildren()).toEqual([]);
// Advance both React's virtual time and Jest's timers by enough to trigger
// the timeout, but not by enough to flush the promise or reach the true
// expiration time.
ReactNoop.expire(120);
await advanceTimers(120);
expect(ReactNoop.flush()).toEqual([
// Still suspended.
'Suspend! [Async]',
// Now that the expiration view has timed out, we render the fallback UI
'Loading...',
'Sync',
]);
expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
// Once the promise resolves, we render the suspended view
await advanceTimers(1000);
expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
});
it('throws a helpful error when a synchronous update is suspended', () => {
expect(() => {
ReactNoop.flushSync(() => ReactNoop.render(<AsyncText text="Async" />));
}).toThrow(
'A synchronous update was suspended, but no fallback UI was provided.',
);
});
it('throws a helpful error when an expired update is suspended', async () => {
ReactNoop.render(<AsyncText text="Async" ms={20000} />);
expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
await advanceTimers(10000);
ReactNoop.expire(10000);
expect(() => {
expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
}).toThrow(
'An update was suspended for longer than the timeout, but no fallback ' +
'UI was provided.',
);
});
it('a Timeout component correctly handles more than one suspended child', async () => {
ReactNoop.render(
<Fallback timeout={0}>
<AsyncText text="A" ms={100} />
<AsyncText text="B" ms={100} />
</Fallback>,
);
ReactNoop.expire(10000);
expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Suspend! [B]']);
expect(ReactNoop.getChildren()).toEqual([]);
await advanceTimers(100);
expect(ReactNoop.flush()).toEqual([
'Promise resolved [A]',
'Promise resolved [B]',
'A',
'B',
]);
expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
});
it('can resume rendering earlier than a timeout', async () => {
ReactNoop.render(
<Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
<AsyncText text="Async" ms={100} />
</Fallback>,
);
expect(ReactNoop.flush()).toEqual(['Suspend! [Async]']);
expect(ReactNoop.getChildren()).toEqual([]);
// Advance time by an amount slightly smaller than what's necessary to
// resolve the promise
await advanceTimers(99);
// Nothing has rendered yet
expect(ReactNoop.flush()).toEqual([]);
expect(ReactNoop.getChildren()).toEqual([]);
// Resolve the promise
await advanceTimers(1);
// We can now resume rendering
expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']);
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
});
describe('splitting a high-pri update into high and low', () => {
React = require('react');
class AsyncValue extends React.Component {
state = {asyncValue: this.props.defaultValue};
componentDidMount() {
ReactNoop.deferredUpdates(() => {
this.setState((state, props) => ({asyncValue: props.value}));
});
}
componentDidUpdate() {
if (this.props.value !== this.state.asyncValue) {
ReactNoop.deferredUpdates(() => {
this.setState((state, props) => ({asyncValue: props.value}));
});
}
}
render() {
return this.props.children(this.state.asyncValue);
}
}
it('coalesces async values when in a suspended state', async () => {
function App(props) {
const highPriText = props.text;
return (
<AsyncValue value={highPriText} defaultValue={null}>
{lowPriText => (
<Fragment>
<Text text={`High-pri: ${highPriText}`} />
{lowPriText && (
<AsyncText text={`Low-pri: ${lowPriText}`} ms={100} />
)}
</Fragment>
)}
</AsyncValue>
);
}
function renderAppSync(props) {
ReactNoop.flushSync(() => ReactNoop.render(<App {...props} />));
}
// Initial mount
renderAppSync({text: 'A'});
expect(ReactNoop.flush()).toEqual([
// First we render at high priority
'High-pri: A',
// Then we come back later to render a low priority
'High-pri: A',
// The low-pri view suspends
'Suspend! [Low-pri: A]',
]);
expect(ReactNoop.getChildren()).toEqual([span('High-pri: A')]);
// Partially flush the promise for 'A', not by enough to resolve it.
await advanceTimers(99);
// Advance React's virtual time so that the next update falls into a new
// expiration bucket
ReactNoop.expire(2000);
// Update to B. At this point, the low-pri view still hasn't updated
// to 'A'.
renderAppSync({text: 'B'});
expect(ReactNoop.flush()).toEqual([
// First we render at high priority
'High-pri: B',
// Then we come back later to render a low priority
'High-pri: B',
// The low-pri view suspends
'Suspend! [Low-pri: B]',
]);
expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]);
// Flush the rest of the promise for 'A', without flushing the one
// for 'B'.
await advanceTimers(1);
expect(ReactNoop.flush()).toEqual([
// A is unblocked
'Promise resolved [Low-pri: A]',
// But we don't try to render it, because there's a lower priority
// update that is also suspended.
]);
expect(ReactNoop.getChildren()).toEqual([span('High-pri: B')]);
// Flush the remaining work.
await advanceTimers(99);
expect(ReactNoop.flush()).toEqual([
// B is unblocked
'Promise resolved [Low-pri: B]',
// Now we can continue rendering the async view
'High-pri: B',
'Low-pri: B',
]);
expect(ReactNoop.getChildren()).toEqual([
span('High-pri: B'),
span('Low-pri: B'),
]);
});
});
describe('a Delay component', () => {
function Never() {
// Throws a promise that resolves after some arbitrarily large
// number of seconds. The idea is that this component will never
// resolve. It's always wrapped by a Timeout.
throw new Promise(resolve => setTimeout(() => resolve(), 10000));
}
function Delay({ms}) {
return (
<Timeout ms={ms}>
{didTimeout => {
if (didTimeout) {
// Once ms has elapsed, render null. This allows the rest of the
// tree to resume rendering.
return null;
}
return <Never />;
}}
</Timeout>
);
}
function DebouncedText({text, ms}) {
return (
<Fragment>
<Delay ms={ms} />
<Text text={text} />
</Fragment>
);
}
it('works', async () => {
ReactNoop.render(<DebouncedText text="A" ms={1000} />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([]);
await advanceTimers(999);
ReactNoop.expire(999);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([]);
await advanceTimers(1);
ReactNoop.expire(1);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('A')]);
});
it('uses the most recent update as its start time', async () => {
ReactNoop.render(<DebouncedText text="A" ms={1000} />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([]);
// Advance time by a little, but not by enough to move this into a new
// expiration bucket.
await advanceTimers(10);
ReactNoop.expire(10);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([]);
// Schedule an update. It should have the same expiration as the first one.
ReactNoop.render(<DebouncedText text="B" ms={1000} />);
// Advance time by enough that it would have timed-out the first update,
// but not enough that it times out the second one.
await advanceTimers(999);
ReactNoop.expire(999);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([]);
// Advance time by just a bit more to trigger the timeout.
await advanceTimers(1);
ReactNoop.expire(1);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('B')]);
});
});
// TODO:
// Timeout inside an async boundary
// Start time of expiration bucket is time of most recent update
// Promise rejection
// Warns if promise reaches the root
// Multiple timeouts with different values
// Suspending inside an offscreen tree
// Timeout for CPU-bound work
});

View File

@@ -1,6 +1,6 @@
{
"name": "react-test-renderer",
"version": "16.3.0-alpha.1",
"version": "16.4.0-alpha.0911da3",
"description": "React package for snapshot testing.",
"main": "index.js",
"repository": "facebook/react",
@@ -20,7 +20,7 @@
"prop-types": "^15.6.0"
},
"peerDependencies": {
"react": "^16.0.0 || 16.3.0-alpha.1"
"react": "^16.0.0 || 16.4.0-alpha.0911da3"
},
"files": [
"LICENSE",

View File

@@ -4,7 +4,7 @@
"keywords": [
"react"
],
"version": "16.3.0-alpha.1",
"version": "16.4.0-alpha.0911da3",
"homepage": "https://reactjs.org/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",

View File

@@ -11,6 +11,8 @@ import {
REACT_FRAGMENT_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_ASYNC_MODE_TYPE,
REACT_LOADING_TYPE,
REACT_TIMEOUT_TYPE,
} from 'shared/ReactSymbols';
import {Component, PureComponent} from './ReactBaseClasses';
@@ -49,6 +51,8 @@ const React = {
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
Loading: REACT_LOADING_TYPE,
Timeout: REACT_TIMEOUT_TYPE,
createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,

View File

@@ -22,6 +22,8 @@ import {
REACT_ASYNC_MODE_TYPE,
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_LOADING_TYPE,
REACT_TIMEOUT_TYPE,
} from 'shared/ReactSymbols';
import checkPropTypes from 'prop-types/checkPropTypes';
import warning from 'fbjs/lib/warning';
@@ -294,6 +296,8 @@ export function createElementWithValidation(type, props, children) {
type === REACT_FRAGMENT_TYPE ||
type === REACT_ASYNC_MODE_TYPE ||
type === REACT_STRICT_MODE_TYPE ||
type === REACT_LOADING_TYPE ||
type === REACT_TIMEOUT_TYPE ||
(typeof type === 'object' &&
type !== null &&
(type.$$typeof === REACT_PROVIDER_TYPE ||

View File

@@ -36,6 +36,12 @@ export const REACT_CONTEXT_TYPE = hasSymbol
export const REACT_ASYNC_MODE_TYPE = hasSymbol
? Symbol.for('react.async_mode')
: 0xeacf;
export const REACT_LOADING_TYPE = hasSymbol
? Symbol.for('react.loading')
: 0xeada;
export const REACT_TIMEOUT_TYPE = hasSymbol
? Symbol.for('react.timeout')
: 0xeadb;
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';

View File

@@ -37,3 +37,5 @@ export const Fragment = 10;
export const Mode = 11;
export const ContextConsumer = 12;
export const ContextProvider = 13;
export const LoadingComponent = 14;
export const TimeoutComponent = 15;

View File

@@ -8,4 +8,4 @@
'use strict';
// TODO: this is special because it gets imported during build.
module.exports = '16.3.0-alpha.1';
module.exports = '16.4.0-alpha.0911da3';

View File

@@ -1,13 +1,18 @@
{
"name": "simple-cache-provider",
"description": "A basic cache for React applications",
"version": "0.2.1",
"version": "0.3.0-alpha.0911da3",
"repository": "facebook/react",
"files": ["LICENSE", "README.md", "index.js", "cjs/"],
"files": [
"LICENSE",
"README.md",
"index.js",
"cjs/"
],
"dependencies": {
"fbjs": "^0.8.16"
},
"peerDependencies": {
"react": "16.3.0-alpha.1"
"react": "^16.3.0-alpha.1 || 16.4.0-alpha.0911da3"
}
}

View File

@@ -259,5 +259,13 @@
"257": "Portals are not currently supported by the server renderer. Render them conditionally so that they only appear on the client render.",
"258": "Unknown element-like object type: %s. This is likely a bug in React. Please file an issue.",
"259": "The experimental Call and Return types are not currently supported by the server renderer.",
"260": "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: %s."
"260": "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: %s.",
"261": "Cannot commit an incomplete root. This error is likely caused by a bug in React. Please file an issue.",
"262": "Expired work should have completed. This error is likely caused by a bug in React. Please file an issue.",
"263": "dispatch: Cannot dispatch during the render phase.",
"264": "A context consumer was rendered with multiple children, or a child that isn't a function. A context consumer expects a single child that is a function. If you did pass a function, make sure there is no trailing or leading whitespace around it.",
"265": "A synchronous update was suspended, but no fallback UI was provided.",
"266": "An update was suspended for longer than the timeout, but no fallback UI was provided.",
"267": "An error logging effect should not have been scheduled if no errors were captured. This error is likely caused by a bug in React. Please file an issue.",
"268": "This unit of work tag cannot capture errors. This error is likely caused by a bug in React. Please file an issue."
}

View File

@@ -4,148 +4,148 @@
"filename": "react.development.js",
"bundleType": "UMD_DEV",
"packageName": "react",
"size": 55674,
"gzip": 15255
"size": 55966,
"gzip": 15341
},
{
"filename": "react.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react",
"size": 6784,
"gzip": 2918
"size": 6898,
"gzip": 2963
},
{
"filename": "react.development.js",
"bundleType": "NODE_DEV",
"packageName": "react",
"size": 46095,
"gzip": 12925
"size": 46387,
"gzip": 13012
},
{
"filename": "react.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react",
"size": 5575,
"gzip": 2457
"size": 5690,
"gzip": 2502
},
{
"filename": "React-dev.js",
"bundleType": "FB_DEV",
"packageName": "react",
"size": 45476,
"gzip": 12448
"size": 45770,
"gzip": 12535
},
{
"filename": "React-prod.js",
"bundleType": "FB_PROD",
"packageName": "react",
"size": 13173,
"gzip": 3600
"size": 13395,
"gzip": 3664
},
{
"filename": "react-dom.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 591513,
"gzip": 138743
"size": 624167,
"gzip": 144074
},
{
"filename": "react-dom.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 96778,
"gzip": 31445
"size": 104362,
"gzip": 33547
},
{
"filename": "react-dom.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 575526,
"gzip": 134516
"size": 608170,
"gzip": 139802
},
{
"filename": "react-dom.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 95503,
"gzip": 30619
"size": 103063,
"gzip": 32747
},
{
"filename": "ReactDOM-dev.js",
"bundleType": "FB_DEV",
"packageName": "react-dom",
"size": 594783,
"gzip": 136782
"size": 629594,
"gzip": 142283
},
{
"filename": "ReactDOM-prod.js",
"bundleType": "FB_PROD",
"packageName": "react-dom",
"size": 279046,
"gzip": 53062
"size": 310070,
"gzip": 57393
},
{
"filename": "react-dom-test-utils.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 41697,
"gzip": 11964
"size": 41809,
"gzip": 12019
},
{
"filename": "react-dom-test-utils.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 10649,
"gzip": 3961
"size": 10655,
"gzip": 3967
},
{
"filename": "react-dom-test-utils.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 36434,
"gzip": 10505
"size": 36546,
"gzip": 10559
},
{
"filename": "react-dom-test-utils.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 10193,
"gzip": 3854
"size": 10199,
"gzip": 3860
},
{
"filename": "ReactTestUtils-dev.js",
"bundleType": "FB_DEV",
"packageName": "react-dom",
"size": 37155,
"gzip": 10582
"size": 37255,
"gzip": 10630
},
{
"filename": "react-dom-unstable-native-dependencies.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 62625,
"gzip": 16444
"size": 62631,
"gzip": 16451
},
{
"filename": "react-dom-unstable-native-dependencies.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 11345,
"gzip": 3920
"size": 11351,
"gzip": 3926
},
{
"filename": "react-dom-unstable-native-dependencies.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 58187,
"gzip": 15163
"size": 58193,
"gzip": 15171
},
{
"filename": "react-dom-unstable-native-dependencies.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 10892,
"gzip": 3787
"size": 10898,
"gzip": 3793
},
{
"filename": "ReactDOMUnstableNativeDependencies-dev.js",
@@ -165,281 +165,281 @@
"filename": "react-dom-server.browser.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-dom",
"size": 102991,
"gzip": 26927
"size": 103257,
"gzip": 27043
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-dom",
"size": 15184,
"gzip": 5856
"size": 15196,
"gzip": 5861
},
{
"filename": "react-dom-server.browser.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 92035,
"gzip": 24618
"size": 92301,
"gzip": 24743
},
{
"filename": "react-dom-server.browser.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 14818,
"gzip": 5705
"size": 14830,
"gzip": 5711
},
{
"filename": "ReactDOMServer-dev.js",
"bundleType": "FB_DEV",
"packageName": "react-dom",
"size": 95165,
"gzip": 24327
"size": 95363,
"gzip": 24408
},
{
"filename": "ReactDOMServer-prod.js",
"bundleType": "FB_PROD",
"packageName": "react-dom",
"size": 33262,
"gzip": 8299
"size": 33268,
"gzip": 8305
},
{
"filename": "react-dom-server.node.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-dom",
"size": 94003,
"gzip": 25175
"size": 94269,
"gzip": 25299
},
{
"filename": "react-dom-server.node.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-dom",
"size": 15642,
"gzip": 6010
"size": 15654,
"gzip": 6015
},
{
"filename": "react-art.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-art",
"size": 389869,
"gzip": 86413
"size": 422520,
"gzip": 91733
},
{
"filename": "react-art.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-art",
"size": 86808,
"gzip": 26944
"size": 94283,
"gzip": 28836
},
{
"filename": "react-art.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-art",
"size": 313942,
"gzip": 67385
"size": 346583,
"gzip": 72642
},
{
"filename": "react-art.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-art",
"size": 50754,
"gzip": 16005
"size": 58257,
"gzip": 18056
},
{
"filename": "ReactART-dev.js",
"bundleType": "FB_DEV",
"packageName": "react-art",
"size": 318024,
"gzip": 66603
"size": 352831,
"gzip": 72092
},
{
"filename": "ReactART-prod.js",
"bundleType": "FB_PROD",
"packageName": "react-art",
"size": 157473,
"gzip": 27225
"size": 188420,
"gzip": 31588
},
{
"filename": "ReactNativeRenderer-dev.js",
"bundleType": "RN_DEV",
"packageName": "react-native-renderer",
"size": 443941,
"gzip": 97414
"size": 478870,
"gzip": 102970
},
{
"filename": "ReactNativeRenderer-prod.js",
"bundleType": "RN_PROD",
"packageName": "react-native-renderer",
"size": 209855,
"gzip": 36492
"size": 240291,
"gzip": 40760
},
{
"filename": "react-test-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 310910,
"gzip": 66329
"size": 343703,
"gzip": 71612
},
{
"filename": "react-test-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
"size": 49219,
"gzip": 15315
"size": 56768,
"gzip": 17425
},
{
"filename": "ReactTestRenderer-dev.js",
"bundleType": "FB_DEV",
"packageName": "react-test-renderer",
"size": 315000,
"gzip": 65520
"size": 349965,
"gzip": 71028
},
{
"filename": "react-test-renderer-shallow.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-test-renderer",
"size": 21221,
"gzip": 5193
"size": 21481,
"gzip": 5316
},
{
"filename": "react-test-renderer-shallow.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-test-renderer",
"size": 6799,
"gzip": 2213
"size": 6805,
"gzip": 2218
},
{
"filename": "ReactShallowRenderer-dev.js",
"bundleType": "FB_DEV",
"packageName": "react-test-renderer",
"size": 20928,
"gzip": 4566
"size": 21120,
"gzip": 4625
},
{
"filename": "react-noop-renderer.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-noop-renderer",
"size": 18777,
"gzip": 5303
"size": 19414,
"gzip": 5488
},
{
"filename": "react-noop-renderer.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-noop-renderer",
"size": 6429,
"gzip": 2573
"size": 6649,
"gzip": 2624
},
{
"filename": "react-reconciler.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 292377,
"gzip": 61765
"size": 325018,
"gzip": 67062
},
{
"filename": "react-reconciler.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 42443,
"gzip": 13358
"size": 49945,
"gzip": 15431
},
{
"filename": "react-reconciler-reflection.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 10934,
"gzip": 3388
"size": 11046,
"gzip": 3441
},
{
"filename": "react-reconciler-reflection.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 2416,
"gzip": 1068
"size": 2422,
"gzip": 1074
},
{
"filename": "react-call-return.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-call-return",
"size": 2691,
"gzip": 965
"size": 2697,
"gzip": 971
},
{
"filename": "react-call-return.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-call-return",
"size": 979,
"gzip": 531
"size": 985,
"gzip": 536
},
{
"filename": "ReactFabric-dev.js",
"bundleType": "RN_DEV",
"packageName": "react-native-renderer",
"size": 438218,
"gzip": 96267
"size": 463717,
"gzip": 99448
},
{
"filename": "ReactFabric-prod.js",
"bundleType": "RN_PROD",
"packageName": "react-native-renderer",
"size": 201883,
"gzip": 35448
"size": 224419,
"gzip": 38115
},
{
"filename": "react-reconciler-persistent.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-reconciler",
"size": 291949,
"gzip": 61587
"size": 324338,
"gzip": 66787
},
{
"filename": "react-reconciler-persistent.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-reconciler",
"size": 41327,
"gzip": 13133
"size": 48804,
"gzip": 15100
},
{
"filename": "react-is.development.js",
"bundleType": "NODE_DEV",
"packageName": "react-is",
"size": 3358,
"gzip": 1015
"size": 3364,
"gzip": 1021
},
{
"filename": "react-is.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "react-is",
"size": 1433,
"gzip": 607
"size": 1439,
"gzip": 612
},
{
"filename": "react-is.development.js",
"bundleType": "UMD_DEV",
"packageName": "react-is",
"size": 3547,
"gzip": 1071
"size": 3553,
"gzip": 1077
},
{
"filename": "react-is.production.min.js",
"bundleType": "UMD_PROD",
"packageName": "react-is",
"size": 1515,
"gzip": 670
"size": 1521,
"gzip": 675
},
{
"filename": "simple-cache-provider.development.js",
"bundleType": "NODE_DEV",
"packageName": "simple-cache-provider",
"size": 5830,
"gzip": 1904
"size": 5836,
"gzip": 1910
},
{
"filename": "simple-cache-provider.production.min.js",
"bundleType": "NODE_PROD",
"packageName": "simple-cache-provider",
"size": 1313,
"gzip": 665
"size": 1319,
"gzip": 671
}
]
}

View File

@@ -4323,7 +4323,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.7, prop-types@^15.6.0:
prop-types@^15.5.7:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@@ -4331,6 +4331,14 @@ prop-types@^15.5.7, prop-types@^15.6.0:
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.6.0:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@~15.5.7:
version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"