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(); + }); +});