diff --git a/examples/fiber/index.html b/examples/fiber/index.html
index c90e7ce246..c965d2f781 100644
--- a/examples/fiber/index.html
+++ b/examples/fiber/index.html
@@ -1,5 +1,5 @@
-
+
Fiber Example
@@ -19,26 +19,154 @@
-
+
diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt
index 034f765d97..da68b422e9 100644
--- a/scripts/fiber/tests-failing.txt
+++ b/scripts/fiber/tests-failing.txt
@@ -71,13 +71,8 @@ src/renderers/shared/shared/__tests__/ReactComponentLifeCycle-test.js
* should carry through each of the phases of setup
src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
-* should warn about `setState` in render
-* should warn about `setState` in getChildContext
* should update refs if shouldComponentUpdate gives false
-src/renderers/shared/shared/__tests__/ReactCompositeComponentState-test.js
-* should update state when called from child cWRP
-
src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
* should still throw when rendering to undefined
* throws when rendering null at the top level
diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt
index 6464c7d7ee..b112148a6e 100644
--- a/scripts/fiber/tests-passing-except-dev.txt
+++ b/scripts/fiber/tests-passing-except-dev.txt
@@ -114,6 +114,8 @@ src/renderers/shared/shared/__tests__/ReactComponentLifeCycle-test.js
src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
* should warn about `forceUpdate` on unmounted components
* should warn about `setState` on unmounted components
+* should warn about `setState` in render
+* should warn about `setState` in getChildContext
* should disallow nested render calls
src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt
index 21a5783ffb..5b821222ca 100644
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -1230,6 +1230,16 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* invokes ref callbacks after insertion/update/unmount
* supports string refs
+src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
+* applies updates in order of priority
+* applies updates with equal priority in insertion order
+* only drops updates with equal or lesser priority when replaceState is called
+* can abort an update, schedule additional updates, and resume
+* can abort an update, schedule a replaceState, and resume
+* does not call callbacks that are scheduled by another callback until a later commit
+* gives setState during reconciliation the same priority as whatever level is currently reconciling
+* enqueues setState inside an updater function as if the in-progress update is progressed (and warns)
+
src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js
* should render a simple fragment at the top of a component
* should preserve state when switching from a single child
@@ -1382,6 +1392,7 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponentState-test.js
* should support setting state
* should call componentDidUpdate of children first
* should batch unmounts
+* should update state when called from child cWRP
src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
* should not produce child DOM nodes for null and false
diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js
index 4c38e66f6b..19c29643f3 100644
--- a/src/renderers/dom/fiber/ReactDOMFiber.js
+++ b/src/renderers/dom/fiber/ReactDOMFiber.js
@@ -242,6 +242,8 @@ var ReactDOM = {
unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
+ unstable_deferredUpdates: DOMRenderer.deferredUpdates,
+
};
module.exports = ReactDOM;
diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js
index d16b6e7bf2..946be9f3b4 100644
--- a/src/renderers/noop/ReactNoop.js
+++ b/src/renderers/noop/ReactNoop.js
@@ -284,17 +284,20 @@ var ReactNoop = {
function logUpdateQueue(updateQueue : UpdateQueue, depth) {
log(
- ' '.repeat(depth + 1) + 'QUEUED UPDATES',
- updateQueue.isReplace ? 'is replace' : '',
- updateQueue.isForced ? 'is forced' : ''
+ ' '.repeat(depth + 1) + 'QUEUED UPDATES'
);
+ const firstUpdate = updateQueue.first;
+ if (!firstUpdate) {
+ return;
+ }
+
log(
' '.repeat(depth + 1) + '~',
- updateQueue.partialState,
- updateQueue.callback ? 'with callback' : ''
+ firstUpdate && firstUpdate.partialState,
+ firstUpdate.callback ? 'with callback' : ''
);
var next;
- while (next = updateQueue.next) {
+ while (next = firstUpdate.next) {
log(
' '.repeat(depth + 1) + '~',
next.partialState,
diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js
index 8873e2b46c..3b9a88fa88 100644
--- a/src/renderers/shared/fiber/ReactFiber.js
+++ b/src/renderers/shared/fiber/ReactFiber.js
@@ -43,6 +43,10 @@ var {
NoEffect,
} = require('ReactTypeOfSideEffect');
+var {
+ cloneUpdateQueue,
+} = require('ReactFiberUpdateQueue');
+
var invariant = require('invariant');
// A Fiber is work on a Component that needs to be done or was done. There can
@@ -100,12 +104,13 @@ export type Fiber = {
pendingProps: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
memoizedProps: any, // The props used to create the output.
- // A queue of local state updates.
- updateQueue: ?UpdateQueue,
- // The state used to create the output. This is a full state object.
+
+ // A queue of state updates and callbacks.
+ updateQueue: UpdateQueue | null,
+ // A list of callbacks that should be called during the next commit.
+ callbackList: UpdateQueue | null,
+ // The state used to create the output
memoizedState: any,
- // Linked list of callbacks to call after updates are committed.
- callbackList: ?UpdateQueue,
// Effect
effectTag: TypeOfSideEffect,
@@ -194,8 +199,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
pendingProps: null,
memoizedProps: null,
updateQueue: null,
- memoizedState: null,
callbackList: null,
+ memoizedState: null,
effectTag: NoEffect,
nextEffect: null,
@@ -270,8 +275,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
// pendingProps is here for symmetry but is unnecessary in practice for now.
// TODO: Pass in the new pendingProps as an argument maybe?
alt.pendingProps = fiber.pendingProps;
- alt.updateQueue = fiber.updateQueue;
- alt.callbackList = fiber.callbackList;
+ cloneUpdateQueue(alt, fiber);
alt.pendingWorkPriority = priorityLevel;
alt.memoizedProps = fiber.memoizedProps;
diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js
index bc98949e6f..94799919b9 100644
--- a/src/renderers/shared/fiber/ReactFiberBeginWork.js
+++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js
@@ -25,7 +25,10 @@ var {
reconcileChildFibersInPlace,
cloneChildFibers,
} = require('ReactChildFiber');
-
+var {
+ hasPendingUpdate,
+ beginUpdateQueue,
+} = require('ReactFiberUpdateQueue');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
getMaskedContext,
@@ -53,6 +56,7 @@ var {
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
+ Update,
Placement,
ContentReset,
Err,
@@ -67,7 +71,10 @@ if (__DEV__) {
module.exports = function(
config : HostConfig,
hostContext : HostContext,
- scheduleUpdate : (fiber: Fiber) => void
+ scheduleSetState: (fiber : Fiber, partialState : any) => void,
+ scheduleReplaceState: (fiber : Fiber, state : any) => void,
+ scheduleForceUpdate: (fiber : Fiber) => void,
+ scheduleUpdateCallback: (fiber : Fiber, callback : Function) => void,
) {
const { shouldSetTextContent } = config;
@@ -84,7 +91,12 @@ module.exports = function(
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
- } = ReactFiberClassComponent(scheduleUpdate);
+ } = ReactFiberClassComponent(
+ scheduleSetState,
+ scheduleReplaceState,
+ scheduleForceUpdate,
+ scheduleUpdateCallback
+ );
function markChildAsProgressed(current, workInProgress, priorityLevel) {
// We now have clones. Let's store them as the currently progressed work.
@@ -195,24 +207,38 @@ module.exports = function(
return workInProgress.child;
}
- function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
+ function updateClassComponent(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) {
let shouldUpdate;
if (!current) {
if (!workInProgress.stateNode) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress);
- mountClassInstance(workInProgress);
+ mountClassInstance(workInProgress, priorityLevel);
shouldUpdate = true;
} else {
// In a resume, we'll already have an instance we can reuse.
- shouldUpdate = resumeMountClassInstance(workInProgress);
+ shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel);
}
} else {
- shouldUpdate = updateClassInstance(current, workInProgress);
+ shouldUpdate = updateClassInstance(current, workInProgress, priorityLevel);
}
- if (!shouldUpdate) {
+
+ // Schedule side-effects
+ if (shouldUpdate) {
+ workInProgress.effectTag |= Update;
+ } else {
+ // If an update was already in progress, we should schedule an Update
+ // effect even though we're bailing out, so that cWU/cDU are called.
+ if (current) {
+ const instance = current.stateNode;
+ if (instance.props !== current.memoizedProps ||
+ instance.state !== current.memoizedState) {
+ workInProgress.effectTag |= Update;
+ }
+ }
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
+
// Rerender
const instance = workInProgress.stateNode;
ReactCurrentOwner.current = workInProgress;
@@ -287,7 +313,7 @@ module.exports = function(
}
}
- function mountIndeterminateComponent(current, workInProgress) {
+ function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
if (current) {
throw new Error('An indeterminate component should never have mounted.');
}
@@ -308,7 +334,7 @@ module.exports = function(
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
adoptClassInstance(workInProgress, value);
- mountClassInstance(workInProgress);
+ mountClassInstance(workInProgress, priorityLevel);
ReactCurrentOwner.current = workInProgress;
value = value.render();
} else {
@@ -471,22 +497,31 @@ module.exports = function(
workInProgress.child = workInProgress.progressedChild;
}
- if ((workInProgress.pendingProps === null || (
- workInProgress.memoizedProps !== null &&
- workInProgress.pendingProps === workInProgress.memoizedProps
- )) &&
- workInProgress.updateQueue === null &&
- !hasContextChanged()) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
+ const pendingProps = workInProgress.pendingProps;
+ const memoizedProps = workInProgress.memoizedProps;
+ const updateQueue = workInProgress.updateQueue;
+
+ // This is kept as a single expression to take advantage of short-circuiting.
+ const hasNewProps = (
+ pendingProps !== null && ( // hasPendingProps && (
+ memoizedProps === null || // hasNoMemoizedProps ||
+ pendingProps !== memoizedProps // memoizedPropsDontMatch
+ ) // )
+ );
+ if (!hasNewProps) {
+ const hasUpdate = updateQueue && hasPendingUpdate(updateQueue, priorityLevel);
+ if (!hasUpdate && !hasContextChanged()) {
+ return bailoutOnAlreadyFinishedWork(current, workInProgress);
+ }
}
switch (workInProgress.tag) {
case IndeterminateComponent:
- return mountIndeterminateComponent(current, workInProgress);
+ return mountIndeterminateComponent(current, workInProgress, priorityLevel);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress);
case ClassComponent:
- return updateClassComponent(current, workInProgress);
+ return updateClassComponent(current, workInProgress, priorityLevel);
case HostRoot: {
const root = (workInProgress.stateNode : FiberRoot);
if (root.pendingContext) {
@@ -497,8 +532,14 @@ module.exports = function(
} else {
pushTopLevelContextObject(root.context, false);
}
+
+ if (updateQueue) {
+ beginUpdateQueue(workInProgress, updateQueue, null, null, null, priorityLevel);
+ }
+
pushHostContainer(workInProgress.stateNode.containerInfo);
- reconcileChildren(current, workInProgress, workInProgress.pendingProps);
+ reconcileChildren(current, workInProgress, pendingProps);
+
// A yield component is just a placeholder, we can just run through the
// next one immediately.
return workInProgress.child;
diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js
index 79595dd152..3772571f8d 100644
--- a/src/renderers/shared/fiber/ReactFiberClassComponent.js
+++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js
@@ -13,17 +13,15 @@
'use strict';
import type { Fiber } from 'ReactFiber';
-import type { UpdateQueue } from 'ReactFiberUpdateQueue';
+import type { PriorityLevel } from 'ReactPriorityLevel';
var {
getMaskedContext,
} = require('ReactFiberContext');
var {
- createUpdateQueue,
- addToQueue,
- addCallbackToQueue,
- mergeUpdateQueue,
+ beginUpdateQueue,
} = require('ReactFiberUpdateQueue');
+var { hasContextChanged } = require('ReactFiberContext');
var { getComponentName, isMounted } = require('ReactFiberTreeReflection');
var ReactInstanceMap = require('ReactInstanceMap');
var shallowEqual = require('shallowEqual');
@@ -32,53 +30,37 @@ var invariant = require('invariant');
const isArray = Array.isArray;
-module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
-
- function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue) {
- fiber.updateQueue = updateQueue;
- // Schedule update on the alternate as well, since we don't know which tree
- // is current.
- if (fiber.alternate) {
- fiber.alternate.updateQueue = updateQueue;
- }
- scheduleUpdate(fiber);
- }
+module.exports = function(
+ scheduleSetState: (fiber : Fiber, partialState : any) => void,
+ scheduleReplaceState: (fiber : Fiber, state : any) => void,
+ scheduleForceUpdate: (fiber : Fiber) => void,
+ scheduleUpdateCallback: (fiber : Fiber, callback : Function) => void,
+) {
// Class component state updater
const updater = {
isMounted,
enqueueSetState(instance, partialState) {
const fiber = ReactInstanceMap.get(instance);
- const updateQueue = fiber.updateQueue ?
- addToQueue(fiber.updateQueue, partialState) :
- createUpdateQueue(partialState);
- scheduleUpdateQueue(fiber, updateQueue);
+ scheduleSetState(fiber, partialState);
},
enqueueReplaceState(instance, state) {
const fiber = ReactInstanceMap.get(instance);
- const updateQueue = createUpdateQueue(state);
- updateQueue.isReplace = true;
- scheduleUpdateQueue(fiber, updateQueue);
+ scheduleReplaceState(fiber, state);
},
enqueueForceUpdate(instance) {
const fiber = ReactInstanceMap.get(instance);
- const updateQueue = fiber.updateQueue || createUpdateQueue(null);
- updateQueue.isForced = true;
- scheduleUpdateQueue(fiber, updateQueue);
+ scheduleForceUpdate(fiber);
},
enqueueCallback(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
- let updateQueue = fiber.updateQueue ?
- fiber.updateQueue :
- createUpdateQueue(null);
- addCallbackToQueue(updateQueue, callback);
- scheduleUpdateQueue(fiber, updateQueue);
+ scheduleUpdateCallback(fiber, callback);
},
};
function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) {
- const updateQueue = workInProgress.updateQueue;
- if (oldProps === null || (updateQueue && updateQueue.isForced)) {
+ if (oldProps === null || (workInProgress.updateQueue && workInProgress.updateQueue.hasForceUpdate)) {
+ // If the workInProgress already has an Update effect, return true
return true;
}
@@ -226,7 +208,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
}
// Invokes the mount life-cycles on a previously never rendered instance.
- function mountClassInstance(workInProgress : Fiber) : void {
+ function mountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : void {
const instance = workInProgress.stateNode;
const state = instance.state || null;
@@ -245,14 +227,21 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// process them now.
const updateQueue = workInProgress.updateQueue;
if (updateQueue) {
- instance.state = mergeUpdateQueue(updateQueue, instance, state, props);
+ instance.state = beginUpdateQueue(
+ workInProgress,
+ updateQueue,
+ instance,
+ state,
+ props,
+ priorityLevel
+ );
}
}
}
// Called on a preexisting class instance. Returns false if a resumed render
// could be reused.
- function resumeMountClassInstance(workInProgress : Fiber) : boolean {
+ function resumeMountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean {
let newState = workInProgress.memoizedState;
let newProps = workInProgress.pendingProps;
if (!newProps) {
@@ -294,13 +283,20 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// during initial mounting.
const newUpdateQueue = workInProgress.updateQueue;
if (newUpdateQueue) {
- newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps);
+ newInstance.state = beginUpdateQueue(
+ workInProgress,
+ newUpdateQueue,
+ newInstance,
+ newState,
+ newProps,
+ priorityLevel
+ );
}
return true;
}
// Invokes the update life-cycles and returns false if it shouldn't rerender.
- function updateClassInstance(current : Fiber, workInProgress : Fiber) : boolean {
+ function updateClassInstance(current : Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps || current.memoizedProps;
@@ -332,19 +328,22 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// TODO: Previous state can be null.
let newState;
if (updateQueue) {
- if (!updateQueue.hasUpdate) {
- newState = oldState;
- } else {
- newState = mergeUpdateQueue(updateQueue, instance, oldState, newProps);
- }
+ newState = beginUpdateQueue(
+ workInProgress,
+ updateQueue,
+ instance,
+ oldState,
+ newProps,
+ priorityLevel
+ );
} else {
newState = oldState;
}
if (oldProps === newProps &&
oldState === newState &&
- oldContext === newContext &&
- updateQueue && !updateQueue.isForced) {
+ !hasContextChanged() &&
+ !(updateQueue && updateQueue.hasForceUpdate)) {
return false;
}
diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js
index 5ff31e2d51..879fcf2485 100644
--- a/src/renderers/shared/fiber/ReactFiberCommitWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js
@@ -25,12 +25,11 @@ var {
HostPortal,
CoroutineComponent,
} = ReactTypeOfWork;
-var { callCallbacks } = require('ReactFiberUpdateQueue');
+var { commitCallbacks } = require('ReactFiberUpdateQueue');
var {
Placement,
Update,
- Callback,
ContentReset,
} = require('ReactTypeOfSideEffect');
@@ -418,25 +417,17 @@ module.exports = function(
}
attachRef(current, finishedWork, instance);
}
- // Clear updates from current fiber.
- if (finishedWork.alternate) {
- finishedWork.alternate.updateQueue = null;
- }
- if (finishedWork.effectTag & Callback) {
- if (finishedWork.callbackList) {
- const callbackList = finishedWork.callbackList;
- finishedWork.callbackList = null;
- callCallbacks(callbackList, instance);
- }
+ const callbackList = finishedWork.callbackList;
+ if (callbackList) {
+ commitCallbacks(finishedWork, callbackList, instance);
}
return;
}
case HostRoot: {
- const rootFiber = finishedWork.stateNode;
- if (rootFiber.callbackList) {
- const callbackList = rootFiber.callbackList;
- rootFiber.callbackList = null;
- callCallbacks(callbackList, rootFiber.current.child.stateNode);
+ const callbackList = finishedWork.callbackList;
+ if (callbackList) {
+ const instance = finishedWork.child && finishedWork.child.stateNode;
+ commitCallbacks(finishedWork, callbackList, instance);
}
return;
}
diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
index d0a6ab217d..9c3a05501a 100644
--- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
@@ -41,7 +41,6 @@ var {
} = ReactTypeOfWork;
var {
Update,
- Callback,
} = ReactTypeOfSideEffect;
if (__DEV__) {
@@ -73,11 +72,6 @@ module.exports = function(
workInProgress.effectTag |= Update;
}
- function markCallback(workInProgress : Fiber) {
- // Tag the fiber with a callback effect.
- workInProgress.effectTag |= Callback;
- }
-
function appendAllYields(yields : Array, workInProgress : Fiber) {
let node = workInProgress.child;
while (node) {
@@ -179,7 +173,7 @@ module.exports = function(
case FunctionalComponent:
workInProgress.memoizedProps = workInProgress.pendingProps;
return null;
- case ClassComponent:
+ case ClassComponent: {
// We are leaving this subtree, so pop context if any.
if (isContextProvider(workInProgress)) {
popContextProvider();
@@ -187,27 +181,13 @@ module.exports = function(
// Don't use the state queue to compute the memoized state. We already
// merged it and assigned it to the instance. Transfer it from there.
// Also need to transfer the props, because pendingProps will be null
- // in the case of an update
- const { state, props } = workInProgress.stateNode;
- const updateQueue = workInProgress.updateQueue;
- workInProgress.memoizedState = state;
- workInProgress.memoizedProps = props;
- if (current) {
- if (current.memoizedProps !== workInProgress.memoizedProps ||
- current.memoizedState !== workInProgress.memoizedState ||
- updateQueue && updateQueue.isForced) {
- markUpdate(workInProgress);
- }
- } else {
- markUpdate(workInProgress);
- }
- if (updateQueue && updateQueue.hasCallback) {
- // Transfer update queue to callbackList field so callbacks can be
- // called during commit phase.
- workInProgress.callbackList = updateQueue;
- markCallback(workInProgress);
- }
+ // in the case of an update.
+ const instance = workInProgress.stateNode;
+ workInProgress.memoizedState = instance.state;
+ workInProgress.memoizedProps = instance.props;
+
return null;
+ }
case HostRoot: {
workInProgress.memoizedProps = workInProgress.pendingProps;
const fiberRoot = (workInProgress.stateNode : FiberRoot);
@@ -215,9 +195,6 @@ module.exports = function(
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
- // TODO: Only mark this as an update if we have any pending callbacks
- // on it.
- markUpdate(workInProgress);
return null;
}
case HostComponent:
diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js
index 582d7946e3..177fc674dc 100644
--- a/src/renderers/shared/fiber/ReactFiberReconciler.js
+++ b/src/renderers/shared/fiber/ReactFiberReconciler.js
@@ -24,8 +24,6 @@ var {
var { createFiberRoot } = require('ReactFiberRoot');
var ReactFiberScheduler = require('ReactFiberScheduler');
-var { createUpdateQueue, addCallbackToQueue } = require('ReactFiberUpdateQueue');
-
if (__DEV__) {
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
}
@@ -79,6 +77,7 @@ export type Reconciler = {
// FIXME: ESLint complains about type parameter
batchedUpdates(fn : () => A) : A,
syncUpdates(fn : () => A) : A,
+ deferredUpdates(fn : () => A) : A,
/* eslint-enable no-undef */
// Used to extract the return value from the initial render. Legacy API.
@@ -99,9 +98,11 @@ module.exports = function(config : HostConfig(config : HostConfig, containerInfo : C, parentComponent : ?ReactComponent, callback: ?Function) : OpaqueNode {
const context = getContextForSubtree(parentComponent);
const root = createFiberRoot(containerInfo, context);
- const container = root.current;
- if (callback) {
- const queue = createUpdateQueue(null);
- addCallbackToQueue(queue, callback);
- root.callbackList = queue;
- }
- // TODO: Use pending work/state instead of props.
+ const current = root.current;
+
+ // TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps.
// TODO: This should not override the pendingWorkPriority if there is
// higher priority work in the subtree.
- container.pendingProps = element;
+
+ current.pendingProps = element;
+ if (current.alternate) {
+ current.alternate.pendingProps = element;
+ }
+ if (callback) {
+ scheduleUpdateCallback(current, callback);
+ }
scheduleWork(root);
@@ -129,24 +133,25 @@ module.exports = function(config : HostConfig, container : OpaqueNode, parentComponent : ?ReactComponent, callback: ?Function) : void {
// TODO: If this is a nested container, this won't be the root.
const root : FiberRoot = (container.stateNode : any);
- if (callback) {
- const queue = root.callbackList ?
- root.callbackList :
- createUpdateQueue(null);
- addCallbackToQueue(queue, callback);
- root.callbackList = queue;
- }
+ const current = root.current;
+
root.pendingContext = getContextForSubtree(parentComponent);
- // TODO: Use pending work/state instead of props.
- root.current.pendingProps = element;
- if (root.current.alternate) {
- root.current.alternate.pendingProps = element;
+
+ // TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps.
+ // TODO: This should not override the pendingWorkPriority if there is
+ // higher priority work in the subtree.
+ current.pendingProps = element;
+ if (current.alternate) {
+ current.alternate.pendingProps = element;
+ }
+ if (callback) {
+ scheduleUpdateCallback(current, callback);
}
scheduleWork(root);
@@ -178,6 +183,8 @@ module.exports = function(config : HostConfig | I | TI | null) {
const root : FiberRoot = (container.stateNode : any);
const containerFiber = root.current;
diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js
index 4da3b9d18e..47ef945dab 100644
--- a/src/renderers/shared/fiber/ReactFiberRoot.js
+++ b/src/renderers/shared/fiber/ReactFiberRoot.js
@@ -13,7 +13,6 @@
'use strict';
import type { Fiber } from 'ReactFiber';
-import type { UpdateQueue } from 'ReactFiberUpdateQueue';
const { createHostRootFiber } = require('ReactFiber');
@@ -26,8 +25,6 @@ export type FiberRoot = {
isScheduled: boolean,
// The work schedule is a linked list.
nextScheduledRoot: ?FiberRoot,
- // Linked list of callbacks to call after updates are committed.
- callbackList: ?UpdateQueue,
// Top context object, used by renderSubtreeIntoContainer
context: Object,
pendingContext: ?Object,
diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js
index 7bf2f7d7aa..88887082d6 100644
--- a/src/renderers/shared/fiber/ReactFiberScheduler.js
+++ b/src/renderers/shared/fiber/ReactFiberScheduler.js
@@ -53,6 +53,14 @@ var {
ClassComponent,
} = require('ReactTypeOfWork');
+var {
+ getPendingPriority,
+ addUpdate,
+ addReplaceUpdate,
+ addForceUpdate,
+ addCallback,
+} = require('ReactFiberUpdateQueue');
+
var {
unwindContext,
} = require('ReactFiberContext');
@@ -67,8 +75,14 @@ var timeHeuristicForUnitOfWork = 1;
module.exports = function(config : HostConfig) {
const hostContext = ReactFiberHostContext(config);
const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
- const { beginWork, beginFailedWork } =
- ReactFiberBeginWork(config, hostContext, scheduleUpdate);
+ const { beginWork, beginFailedWork } = ReactFiberBeginWork(
+ config,
+ hostContext,
+ scheduleSetState,
+ scheduleReplaceState,
+ scheduleForceUpdate,
+ scheduleUpdateCallback,
+ );
const { completeWork } = ReactFiberCompleteWork(config, hostContext);
const {
commitPlacement,
@@ -85,10 +99,15 @@ module.exports = function(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(fn : () => A) : A {
+ const previousPriorityContext = priorityContext;
+ priorityContext = LowPriority;
+ try {
+ return fn();
+ } finally {
+ priorityContext = previousPriorityContext;
+ }
+ }
+
return {
scheduleWork: scheduleWork,
+ scheduleUpdateCallback: scheduleUpdateCallback,
performWithPriority: performWithPriority,
batchedUpdates: batchedUpdates,
syncUpdates: syncUpdates,
+ deferredUpdates: deferredUpdates,
};
};
diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
index 5fe2eafdbe..50f8ceeb47 100644
--- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
@@ -12,100 +12,479 @@
'use strict';
-type UpdateQueueNode = {
- partialState: any,
- callback: ?Function,
+import type { Fiber } from 'ReactFiber';
+import type { PriorityLevel } from 'ReactPriorityLevel';
+
+const {
+ Callback: CallbackEffect,
+} = require('ReactTypeOfSideEffect');
+
+const {
+ NoWork,
+ SynchronousPriority,
+ TaskPriority,
+} = require('ReactPriorityLevel');
+
+type PartialState =
+ $Subtype |
+ (prevState: State, props: Props) => $Subtype;
+
+type Callback = () => void;
+
+type Update = {
+ priorityLevel: PriorityLevel,
+ partialState: PartialState,
+ callback: Callback | null,
isReplace: boolean,
- next: ?UpdateQueueNode,
-};
-
-export type UpdateQueue = UpdateQueueNode & {
isForced: boolean,
- hasUpdate: boolean,
- hasCallback: boolean,
- tail: UpdateQueueNode
+ next: Update | null,
};
-exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue {
- const queue = {
- partialState,
- callback: null,
- isReplace: false,
- next: null,
- isForced: false,
- hasUpdate: partialState != null,
- hasCallback: false,
- tail: (null : any),
- };
- queue.tail = queue;
- return queue;
+// Singly linked-list of updates. When an update is scheduled, it is added to
+// the queue of the current fiber and the work-in-progress fiber. The two queues
+// are separate but they share a persistent structure.
+//
+// During reconciliation, updates are removed from the work-in-progress fiber,
+// but they remain on the current fiber. That ensures that if a work-in-progress
+// is aborted, the aborted updates are recovered by cloning from current.
+//
+// The work-in-progress queue is always a subset of the current queue.
+//
+// When the tree is committed, the work-in-progress becomes the current.
+export type UpdateQueue = {
+ first: Update | null,
+ last: Update | null,
+ hasForceUpdate: boolean,
+
+ // Dev only
+ isProcessing?: boolean,
};
-function addToQueue(queue : UpdateQueue, partialState : mixed) : UpdateQueue {
- const node = {
- partialState,
- callback: null,
- isReplace: false,
- next: null,
- };
- queue.tail.next = node;
- queue.tail = node;
- queue.hasUpdate = queue.hasUpdate || (partialState != null);
+function comparePriority(a : PriorityLevel, b : PriorityLevel) : number {
+ // When comparing update priorities, treat sync and Task work as equal.
+ // TODO: Could we avoid the need for this by always coercing sync priority
+ // to Task when scheduling an update?
+ if ((a === TaskPriority || a === SynchronousPriority) &&
+ (b === TaskPriority || b === SynchronousPriority)) {
+ return 0;
+ }
+ if (a === NoWork && b !== NoWork) {
+ return -255;
+ }
+ if (a !== NoWork && b === NoWork) {
+ return 255;
+ }
+ return a - b;
+}
+
+function hasPendingUpdate(queue : UpdateQueue, priorityLevel : PriorityLevel) : boolean {
+ if (!queue.first) {
+ return false;
+ }
+ // Return true if the first pending update has greater or equal priority.
+ return comparePriority(queue.first.priorityLevel, priorityLevel) <= 0;
+}
+exports.hasPendingUpdate = hasPendingUpdate;
+
+// Ensures that a fiber has an update queue, creating a new one if needed.
+// Returns the new or existing queue.
+function ensureUpdateQueue(fiber : Fiber) : UpdateQueue {
+ if (fiber.updateQueue) {
+ // We already have an update queue.
+ return fiber.updateQueue;
+ }
+
+ let queue;
+ if (__DEV__) {
+ queue = {
+ first: null,
+ last: null,
+ hasForceUpdate: false,
+ isProcessing: false,
+ };
+ } else {
+ queue = {
+ first: null,
+ last: null,
+ hasForceUpdate: false,
+ };
+ }
+
+ fiber.updateQueue = queue;
return queue;
}
-exports.addToQueue = addToQueue;
-
-exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : UpdateQueue {
- if (queue.tail.callback) {
- // If the tail already as a callback, add an empty node to queue
- addToQueue(queue, null);
+// Clones an update queue from a source fiber onto its alternate.
+function cloneUpdateQueue(alt : Fiber, fiber : Fiber) : UpdateQueue | null {
+ const sourceQueue = fiber.updateQueue;
+ if (!sourceQueue) {
+ // The source fiber does not have an update queue.
+ alt.updateQueue = null;
+ return null;
}
- queue.tail.callback = callback;
- queue.hasCallback = true;
- return queue;
-};
+ // If the alternate already has a queue, reuse the previous object.
+ const altQueue = alt.updateQueue || {};
+ altQueue.first = sourceQueue.first;
+ altQueue.last = sourceQueue.last;
+ altQueue.hasForceUpdate = sourceQueue.hasForceUpdate;
+ alt.updateQueue = altQueue;
+ return altQueue;
+}
+exports.cloneUpdateQueue = cloneUpdateQueue;
-exports.callCallbacks = function(queue : UpdateQueue, context : any) {
- let node : ?UpdateQueueNode = queue;
- while (node) {
- const callback = node.callback;
- if (callback) {
- if (typeof context !== 'undefined') {
- callback.call(context);
- } else {
- callback();
+function cloneUpdate(update : Update) : Update {
+ return {
+ priorityLevel: update.priorityLevel,
+ partialState: update.partialState,
+ callback: update.callback,
+ isReplace: update.isReplace,
+ isForced: update.isForced,
+ next: null,
+ };
+}
+
+function insertUpdateIntoQueue(queue, update, insertAfter, insertBefore) {
+ if (insertAfter) {
+ insertAfter.next = update;
+ } else {
+ // This is the first item in the queue.
+ update.next = queue.first;
+ queue.first = update;
+ }
+
+ if (insertBefore) {
+ update.next = insertBefore;
+ } else {
+ // This is the last item in the queue.
+ queue.last = update;
+ }
+}
+
+// The work-in-progress queue is a subset of the current queue (if it exists).
+// We need to insert the incoming update into both lists. However, it's possible
+// that the correct position in one list will be different from the position in
+// the other. Consider the following case:
+//
+// Current: 3-5-6
+// Work-in-progress: 6
+//
+// Then we receive an update with priority 4 and insert it into each list:
+//
+// Current: 3-4-5-6
+// Work-in-progress: 4-6
+//
+// In the current queue, the new update's `next` pointer points to the update
+// with priority 5. But in the work-in-progress queue, the pointer points to the
+// update with priority 6. Because these two queues share the same persistent
+// data structure, this won't do. (This can only happen when the incoming update
+// has higher priority than all the updates in the work-in-progress queue.)
+//
+// To solve this, in the case where the incoming update needs to be inserted
+// into two different positions, we'll make a clone of the update and insert
+// each copy into a separate queue. This forks the list while maintaining a
+// persistent stucture, because the update that is added to the work-in-progress
+// is always added to the front of the list.
+//
+// However, if incoming update is inserted into the same position of both lists,
+// we shouldn't make a copy.
+
+function insertUpdate(fiber : Fiber, update : Update, methodName : ?string) : void {
+ const queue1 = ensureUpdateQueue(fiber);
+ const queue2 = fiber.alternate ? ensureUpdateQueue(fiber.alternate) : null;
+
+ // Warn if an update is scheduled from inside an updater function.
+ if (__DEV__ && typeof methodName === 'string' && (queue1.isProcessing || (queue2 && queue2.isProcessing))) {
+ if (methodName === 'setState') {
+ console.error(
+ 'setState was called from inside the updater function of another' +
+ 'setState. A function passed as the first argument of setState ' +
+ 'should not contain any side-effects. Return a partial state object ' +
+ 'instead of calling setState again. Example: ' +
+ 'this.setState(function(state) { return { count: state.count + 1 }; })'
+ );
+ } else {
+ console.error(
+ `${methodName} was called from inside the updater function of ` +
+ 'setState. A function passed as the first argument of setState ' +
+ 'should not contain any side-effects.'
+ );
+ }
+ }
+
+ const priorityLevel = update.priorityLevel;
+
+ let queue = queue1;
+ let insertAfter1;
+ let insertBefore1;
+ let insertAfter2;
+ let insertBefore2;
+ for (let i = 0; queue && i < 2; i++) {
+ let insertAfter = null;
+ let insertBefore = null;
+ if (queue.last && comparePriority(queue.last.priorityLevel, priorityLevel) <= 0) {
+ // Fast path for the common case where the update should be inserted at
+ // the end of the queue.
+ insertAfter = queue.last;
+ } else {
+ insertBefore = queue.first;
+ while (insertBefore && comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0) {
+ insertAfter = insertBefore;
+ insertBefore = insertBefore.next;
}
}
- node = node.next;
+ if (i === 0) {
+ insertAfter1 = insertAfter;
+ insertBefore1 = insertBefore;
+ queue = queue2;
+ } else {
+ insertAfter2 = insertAfter;
+ insertBefore2 = insertBefore;
+ queue = null;
+ }
}
-};
-function getStateFromNode(node, instance, state, props) {
- if (typeof node.partialState === 'function') {
- const updateFn = node.partialState;
- return updateFn.call(instance, state, props);
- } else {
- return node.partialState;
+ const update1 = update;
+ insertUpdateIntoQueue(queue1, update1, insertAfter1, insertBefore1);
+
+ if (queue2) {
+ let update2;
+ if (insertBefore1 === insertBefore2) {
+ // The update is inserted into the same position of both lists. There's no
+ // need to clone the update.
+ update2 = update1;
+ } else {
+ // The update is inserted into two separate positions. Make a clone of the
+ // update and insert it twice. One or the other will be dropped the next
+ // time we commit.
+ update2 = cloneUpdate(update1);
+ }
+ insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
}
}
-exports.mergeUpdateQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any {
- let node : ?UpdateQueueNode = queue;
- if (queue.isReplace) {
- // replaceState is always first in the queue.
- prevState = getStateFromNode(queue, instance, prevState, props);
- node = queue.next;
- if (!node) {
- // If there is no more work, we replace the raw object instead of cloning.
- return prevState;
+function addUpdate(
+ fiber : Fiber,
+ partialState : PartialState | null,
+ priorityLevel : PriorityLevel
+) : void {
+ const update = {
+ priorityLevel,
+ partialState,
+ callback: null,
+ isReplace: false,
+ isForced: false,
+ next: null,
+ };
+ if (__DEV__) {
+ insertUpdate(fiber, update, 'setState');
+ } else {
+ insertUpdate(fiber, update);
+ }
+}
+exports.addUpdate = addUpdate;
+
+function addReplaceUpdate(
+ fiber : Fiber,
+ state : any | null,
+ priorityLevel : PriorityLevel
+) : void {
+ const update = {
+ priorityLevel,
+ partialState: state,
+ callback: null,
+ isReplace: true,
+ isForced: false,
+ next: null,
+ };
+
+ // Drop all updates with equal priority
+ let queue = ensureUpdateQueue(fiber);
+ for (let i = 0; queue && i < 2; i++) {
+ let replaceAfter = null;
+ let replaceBefore = queue.first;
+ let comparison = 255;
+ while (replaceBefore &&
+ (comparison = comparePriority(replaceBefore.priorityLevel, priorityLevel)) <= 0) {
+ if (comparison < 0) {
+ replaceAfter = replaceBefore;
+ }
+ replaceBefore = replaceBefore.next;
+ }
+
+ if (replaceAfter) {
+ replaceAfter.next = replaceBefore;
+ } else {
+ queue.first = replaceBefore;
+ }
+
+ if (!replaceBefore) {
+ queue.last = replaceAfter;
+ }
+
+ if (fiber.alternate) {
+ queue = ensureUpdateQueue(fiber.alternate);
+ } else {
+ queue = null;
}
}
- let state = Object.assign({}, prevState);
- while (node) {
- let partialState = getStateFromNode(node, instance, state, props);
- Object.assign(state, partialState);
- node = node.next;
+ if (__DEV__) {
+ insertUpdate(fiber, update, 'replaceState');
+ } else {
+ insertUpdate(fiber, update);
}
+}
+exports.addReplaceUpdate = addReplaceUpdate;
+
+function addForceUpdate(fiber : Fiber, priorityLevel : PriorityLevel) : void {
+ const update = {
+ priorityLevel,
+ partialState: null,
+ callback: null,
+ isReplace: false,
+ isForced: true,
+ next: null,
+ };
+ if (__DEV__) {
+ insertUpdate(fiber, update, 'forceUpdate');
+ } else {
+ insertUpdate(fiber, update);
+ }
+}
+exports.addForceUpdate = addForceUpdate;
+
+
+function addCallback(fiber : Fiber, callback: Callback, priorityLevel : PriorityLevel) : void {
+ const update : Update = {
+ priorityLevel,
+ partialState: null,
+ callback,
+ isReplace: false,
+ isForced: false,
+ next: null,
+ };
+ insertUpdate(fiber, update);
+}
+exports.addCallback = addCallback;
+
+function getPendingPriority(queue : UpdateQueue) : PriorityLevel {
+ return queue.first ? queue.first.priorityLevel : NoWork;
+}
+exports.getPendingPriority = getPendingPriority;
+
+function getStateFromUpdate(update, instance, prevState, props) {
+ const partialState = update.partialState;
+ if (typeof partialState === 'function') {
+ const updateFn = partialState;
+ return updateFn.call(instance, prevState, props);
+ } else {
+ return partialState;
+ }
+}
+
+function beginUpdateQueue(
+ workInProgress : Fiber,
+ queue : UpdateQueue,
+ instance : any,
+ prevState : any,
+ props : any,
+ priorityLevel : PriorityLevel
+) : any {
+ if (__DEV__) {
+ // Set this flag so we can warn if setState is called inside the update
+ // function of another setState.
+ queue.isProcessing = true;
+ }
+
+ queue.hasForceUpdate = false;
+
+ // Applies updates with matching priority to the previous state to create
+ // a new state object.
+ let state = prevState;
+ let dontMutatePrevState = true;
+ let isEmpty = true;
+ let callbackList = null;
+ let update = queue.first;
+ while (update && comparePriority(update.priorityLevel, priorityLevel) <= 0) {
+ // Remove each update from the queue right before it is processed. That way
+ // if setState is called from inside an updater function, the new update
+ // will be inserted in the correct position.
+ queue.first = update.next;
+ if (!queue.first) {
+ queue.last = null;
+ }
+
+ let partialState;
+ if (update.isReplace) {
+ // A replace should drop all previous updates in the queue, so
+ // use the original `prevState`, not the accumulated `state`
+ state = getStateFromUpdate(update, instance, prevState, props);
+ dontMutatePrevState = true;
+ isEmpty = false;
+ } else {
+ partialState = getStateFromUpdate(update, instance, state, props);
+ if (partialState) {
+ if (dontMutatePrevState) {
+ state = Object.assign({}, state, partialState);
+ } else {
+ state = Object.assign(state, partialState);
+ }
+ dontMutatePrevState = false;
+ isEmpty = false;
+ }
+ }
+ if (update.isForced) {
+ queue.hasForceUpdate = true;
+ }
+ if (update.callback) {
+ if (callbackList && callbackList.last) {
+ callbackList.last.next = update;
+ callbackList.last = update;
+ } else {
+ callbackList = {
+ first: update,
+ last: update,
+ hasForceUpdate: false,
+ };
+ }
+ workInProgress.effectTag |= CallbackEffect;
+ }
+ update = update.next;
+ }
+
+ if (isEmpty) {
+ // None of the updates contained state. Use the original state object.
+ state = prevState;
+ }
+
+ if (!queue.first && !queue.hasForceUpdate) {
+ // Queue is now empty
+ workInProgress.updateQueue = null;
+ }
+
+ workInProgress.callbackList = callbackList;
+ workInProgress.memoizedState = state;
+
+ if (__DEV__) {
+ queue.isProcessing = false;
+ }
+
return state;
-};
+}
+exports.beginUpdateQueue = beginUpdateQueue;
+
+function commitCallbacks(finishedWork : Fiber, callbackList : UpdateQueue, context : mixed) {
+ const stopAfter = callbackList.last;
+ let update = callbackList.first;
+ while (update) {
+ const callback = update.callback;
+ if (typeof callback === 'function') {
+ callback.call(context);
+ }
+ if (update === stopAfter) {
+ break;
+ }
+ update = update.next;
+ }
+ finishedWork.callbackList = null;
+}
+exports.commitCallbacks = commitCallbacks;
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
new file mode 100644
index 0000000000..1b6627e2ee
--- /dev/null
+++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
@@ -0,0 +1,354 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React;
+var ReactNoop;
+
+describe('ReactIncrementalUpdates', () => {
+ beforeEach(() => {
+ jest.resetModuleRegistry();
+ React = require('React');
+ ReactNoop = require('ReactNoop');
+ });
+
+ it('applies updates in order of priority', () => {
+ let state;
+ class Foo extends React.Component {
+ state = {};
+ componentDidMount() {
+ ReactNoop.performAnimationWork(() => {
+ // Has Animation priority
+ this.setState({ b: 'b' });
+ this.setState({ c: 'c' });
+ });
+ // Has Task priority
+ this.setState({ a: 'a' });
+ }
+ render() {
+ state = this.state;
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flushDeferredPri(25);
+ expect(state).toEqual({ a: 'a' });
+ ReactNoop.flush();
+ expect(state).toEqual({ a: 'a', b: 'b', c: 'c' });
+ });
+
+ it('applies updates with equal priority in insertion order', () => {
+ let state;
+ class Foo extends React.Component {
+ state = {};
+ componentDidMount() {
+ // All have Task priority
+ this.setState({ a: 'a' });
+ this.setState({ b: 'b' });
+ this.setState({ c: 'c' });
+ }
+ render() {
+ state = this.state;
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(state).toEqual({ a: 'a', b: 'b', c: 'c' });
+ });
+
+ it('only drops updates with equal or lesser priority when replaceState is called', () => {
+ let instance;
+ let ops = [];
+ const Foo = React.createClass({
+ getInitialState() {
+ return {};
+ },
+ componentDidMount() {
+ ops.push('componentDidMount');
+ },
+ componentDidUpdate() {
+ ops.push('componentDidUpdate');
+ },
+ render() {
+ ops.push('render');
+ instance = this;
+ return ;
+ },
+ });
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ instance.setState({ x: 'x' });
+ instance.setState({ y: 'y' });
+ ReactNoop.performAnimationWork(() => {
+ instance.setState({ a: 'a' });
+ instance.setState({ b: 'b' });
+ });
+ instance.replaceState({ c: 'c' });
+ instance.setState({ d: 'd' });
+
+ ReactNoop.flushAnimationPri();
+ // Even though a replaceState has been already scheduled, it hasn't been
+ // flushed yet because it has low priority.
+ expect(instance.state).toEqual({ a: 'a', b: 'b' });
+ expect(ops).toEqual([
+ 'render',
+ 'componentDidMount',
+ 'render',
+ 'componentDidUpdate',
+ ]);
+
+ ops = [];
+
+ ReactNoop.flush();
+ // Now the rest of the updates are flushed.
+ expect(instance.state).toEqual({ c: 'c', d: 'd' });
+ expect(ops).toEqual([
+ 'render',
+ 'componentDidUpdate',
+ ]);
+ });
+
+ it('can abort an update, schedule additional updates, and resume', () => {
+ let instance;
+ let ops = [];
+ class Foo extends React.Component {
+ state = {};
+ componentDidUpdate() {
+ ops.push('componentDidUpdate');
+ }
+ render() {
+ ops.push('render');
+ instance = this;
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ops = [];
+
+ let progressedUpdates = [];
+ function createUpdate(letter) {
+ return () => {
+ progressedUpdates.push(letter);
+ return {
+ [letter]: letter,
+ };
+ };
+ }
+
+ instance.setState(createUpdate('a'));
+ instance.setState(createUpdate('b'));
+ instance.setState(createUpdate('c'));
+
+ // Do just enough work to begin the update but not enough to flush it
+ ReactNoop.flushDeferredPri(15);
+ // expect(ReactNoop.getChildren()).toEqual([span('')]);
+ expect(ops).toEqual(['render']);
+ expect(progressedUpdates).toEqual(['a', 'b', 'c']);
+ expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c' });
+
+ ops = [];
+ progressedUpdates = [];
+
+ instance.setState(createUpdate('f'));
+ ReactNoop.performAnimationWork(() => {
+ instance.setState(createUpdate('d'));
+ instance.setState(createUpdate('e'));
+ });
+ instance.setState(createUpdate('g'));
+
+ ReactNoop.flushAnimationPri();
+ expect(ops).toEqual([
+ // Flushes animation work (d and e)
+ 'render',
+ 'componentDidUpdate',
+ ]);
+ ops = [];
+ ReactNoop.flush();
+ expect(ops).toEqual([
+ // Flushes deferred work (f and g)
+ 'render',
+ 'componentDidUpdate',
+ ]);
+ expect(progressedUpdates).toEqual(['d', 'e', 'a', 'b', 'c', 'f', 'g']);
+ expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g' });
+ });
+
+ it('can abort an update, schedule a replaceState, and resume', () => {
+ let instance;
+ let ops = [];
+ const Foo = React.createClass({
+ getInitialState() {
+ return {};
+ },
+ componentDidUpdate() {
+ ops.push('componentDidUpdate');
+ },
+ render() {
+ ops.push('render');
+ instance = this;
+ return (
+
+ );
+ },
+ });
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ ops = [];
+
+ let progressedUpdates = [];
+ function createUpdate(letter) {
+ return () => {
+ progressedUpdates.push(letter);
+ return {
+ [letter]: letter,
+ };
+ };
+ }
+
+ instance.setState(createUpdate('a'));
+ instance.setState(createUpdate('b'));
+ instance.setState(createUpdate('c'));
+
+ // Do just enough work to begin the update but not enough to flush it
+ ReactNoop.flushDeferredPri(20);
+ expect(ops).toEqual(['render']);
+ expect(progressedUpdates).toEqual(['a', 'b', 'c']);
+ expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c' });
+
+ ops = [];
+ progressedUpdates = [];
+
+ instance.setState(createUpdate('f'));
+ ReactNoop.performAnimationWork(() => {
+ instance.setState(createUpdate('d'));
+ instance.replaceState(createUpdate('e'));
+ });
+ instance.setState(createUpdate('g'));
+
+ ReactNoop.flush();
+ // Ensure that updater function d is never called.
+ expect(progressedUpdates).toEqual(['e', 'f', 'g']);
+ expect(instance.state).toEqual({ e: 'e', f: 'f', g: 'g' });
+ });
+
+ it('does not call callbacks that are scheduled by another callback until a later commit', () => {
+ let ops = [];
+ class Foo extends React.Component {
+ state = {};
+ componentDidMount() {
+ ops.push('did mount');
+ this.setState({ a: 'a' }, () => {
+ ops.push('callback a');
+ this.setState({ b: 'b' }, () => {
+ ops.push('callback b');
+ });
+ });
+ }
+ render() {
+ ops.push('render');
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ops).toEqual([
+ 'render',
+ 'did mount',
+ 'render',
+ 'callback a',
+ 'render',
+ 'callback b',
+ ]);
+ });
+
+ it('gives setState during reconciliation the same priority as whatever level is currently reconciling', () => {
+ let instance;
+ let ops = [];
+
+ class Foo extends React.Component {
+ state = {};
+ componentWillReceiveProps() {
+ ops.push('componentWillReceiveProps');
+ this.setState({ b: 'b' });
+ }
+ render() {
+ ops.push('render');
+ instance = this;
+ return ;
+ }
+ }
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ ops = [];
+
+ ReactNoop.performAnimationWork(() => {
+ instance.setState({ a: 'a' });
+ ReactNoop.render(); // Trigger componentWillReceiveProps
+ });
+ ReactNoop.flush();
+
+ expect(instance.state).toEqual({ a: 'a', b: 'b' });
+ expect(ops).toEqual([
+ 'componentWillReceiveProps',
+ 'render',
+ ]);
+ });
+
+
+ it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => {
+ spyOn(console, 'error');
+ let instance;
+ let ops = [];
+ class Foo extends React.Component {
+ state = {};
+ render() {
+ ops.push('render');
+ instance = this;
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ instance.setState(function a() {
+ ops.push('setState updater');
+ this.setState({ b: 'b' });
+ return { a: 'a' };
+ });
+
+ ReactNoop.flush();
+ expect(ops).toEqual([
+ // Initial render
+ 'render',
+ 'setState updater',
+ // Update b is enqueued with the same priority as update a, so it should
+ // be flushed in the same commit.
+ 'render',
+ ]);
+ expect(instance.state).toEqual({ a: 'a', b: 'b' });
+
+ expectDev(console.error.calls.count()).toBe(1);
+ console.error.calls.reset();
+ });
+});