Coalesce like-priority updates made to the same component

Maybe it's ok to do this across components? It would make this much simpler.
This commit is contained in:
Andrew Clark
2018-02-23 15:28:56 -08:00
parent 9a5f3753ba
commit 0911da3f8e
10 changed files with 316 additions and 147 deletions
+8 -5
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,
@@ -92,11 +93,12 @@ 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,
recalculateCurrentTime: () => ExpirationTime,
computeExpirationTimeForPriority: (
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
) => ExpirationTime,
) {
const {shouldSetTextContent, shouldDeprioritizeSubtree} = config;
@@ -117,10 +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.
+24 -7
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 {
@@ -115,13 +116,14 @@ export default function(
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) => void,
computeExpirationForFiber: (
startTime: ExpirationTime,
fiber: Fiber,
) => ExpirationTime,
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 = {
@@ -132,10 +134,15 @@ export default function(
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
priorityLevel,
partialState,
callback,
isReplace: false,
@@ -152,10 +159,15 @@ export default function(
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
priorityLevel,
partialState: state,
callback,
isReplace: true,
@@ -172,11 +184,16 @@ export default function(
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
const priorityLevel = computeUpdatePriorityForFiber(fiber);
const currentTime = recalculateCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
partialState: null,
priorityLevel,
callback,
isReplace: false,
isForced: true,
+12 -5
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,
@@ -92,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;
@@ -156,10 +158,15 @@ 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 = computeExpirationForFiber(currentTime, fiber);
const expirationTime = computeExpirationTimeForPriority(
priorityLevel,
currentTime,
);
const update = {
expirationTime,
priorityLevel,
partialState: false,
callback: null,
isReplace: true,
+41 -43
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,
@@ -360,45 +388,6 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
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(currentTime, 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,
);
+78 -39
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';
@@ -80,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';
@@ -180,8 +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,
@@ -208,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,
@@ -228,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;
@@ -990,6 +1000,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
sourceFiber,
boundaryFiber,
value,
priorityLevel,
startTime,
expirationTime,
) {
@@ -997,6 +1008,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const capturedValue = createCapturedValue(value, sourceFiber);
const update = {
expirationTime,
priorityLevel,
partialState: null,
callback: null,
isReplace: false,
@@ -1011,6 +1023,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
function dispatch(
sourceFiber: Fiber,
value: mixed,
priorityLevel: PriorityLevel,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) {
@@ -1036,6 +1049,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
sourceFiber,
fiber,
value,
priorityLevel,
startTime,
expirationTime,
);
@@ -1044,7 +1058,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
break;
// TODO: Handle async boundaries
case HostRoot:
scheduleCapture(sourceFiber, fiber, value, startTime, expirationTime);
scheduleCapture(
sourceFiber,
fiber,
value,
priorityLevel,
startTime,
expirationTime,
);
return;
}
fiber = fiber.return;
@@ -1057,6 +1078,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
sourceFiber,
sourceFiber,
value,
priorityLevel,
startTime,
expirationTime,
);
@@ -1065,7 +1087,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
function onCommitPhaseError(fiber: Fiber, error: mixed) {
const startTime = recalculateCurrentTime();
return dispatch(fiber, error, startTime, Sync);
return dispatch(fiber, error, SyncPriority, startTime, Sync);
}
function computeAsyncExpiration(currentTime: ExpirationTime) {
@@ -1098,23 +1120,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return lastUniqueAsyncExpiration;
}
function computeExpirationForFiber(
currentTime: ExpirationTime,
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
@@ -1122,28 +1141,36 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
if (fiber.mode & AsyncMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update
expirationTime = computeInteractiveExpiration(currentTime);
priorityLevel = InteractivePriority;
} else {
// This is an async update
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 priorityLevel;
}
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);
}
return expirationTime;
}
function retryOnPromiseResolution(
@@ -1193,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
@@ -1262,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>(
@@ -1278,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;
}
}
@@ -1824,7 +1862,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
return {
recalculateCurrentTime,
computeExpirationForFiber,
computeUpdatePriorityForFiber,
computeExpirationTimeForPriority,
scheduleWork,
requestWork,
flushRoot,
+10 -3
View File
@@ -29,6 +29,7 @@ import {
ShouldCapture,
} from 'shared/ReactTypeOfSideEffect';
import {Sync} from './ReactFiberExpirationTime';
import {NoPriority} from './ReactPriorityLevel';
import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags';
@@ -85,9 +86,10 @@ export default function(
renderStartTime,
renderExpirationTime,
) {
const slightlyHigherPriority = renderExpirationTime - 1;
const slightlyEarlierExpirationTime = renderExpirationTime - 1;
const loadingUpdate = {
expirationTime: slightlyHigherPriority,
expirationTime: slightlyEarlierExpirationTime,
priorityLevel: NoPriority,
partialState: true,
callback: null,
isReplace: true,
@@ -99,6 +101,7 @@ export default function(
const revertUpdate = {
expirationTime: renderExpirationTime,
priorityLevel: NoPriority,
partialState: false,
callback: null,
isReplace: true,
@@ -107,7 +110,11 @@ export default function(
next: null,
};
insertUpdateIntoFiber(workInProgress, revertUpdate);
scheduleWork(workInProgress, renderStartTime, slightlyHigherPriority);
scheduleWork(
workInProgress,
renderStartTime,
slightlyEarlierExpirationTime,
);
return false;
}
+24
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 {
@@ -26,6 +27,7 @@ import warning from 'fbjs/lib/warning';
import {StrictMode} from './ReactTypeOfMode';
import {NoWork} from './ReactFiberExpirationTime';
import {NoPriority} from './ReactPriorityLevel';
let didWarnUpdateInsideUpdate;
@@ -42,6 +44,7 @@ type Callback = mixed;
export type Update<State> = {
expirationTime: ExpirationTime,
priorityLevel: PriorityLevel,
partialState: PartialState<any, any>,
callback: Callback | null,
isReplace: boolean,
@@ -101,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
+16
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;
@@ -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')]);
});
});
@@ -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')]);
});
});