diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 52a6346ffb..0a0cebddae 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -32,6 +32,8 @@ import {StrictMode} from './ReactTypeOfMode'; import { enqueueUpdate, processUpdateQueue, + checkHasForceUpdateAfterProcessing, + resetHasForceUpdateBeforeProcessing, createUpdate, ReplaceState, ForceUpdate, @@ -235,14 +237,6 @@ export default function( newState, newContext, ) { - if ( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) { - // If forceUpdate was called, disregard sCU. - return true; - } - const instance = workInProgress.stateNode; const ctor = workInProgress.type; if (typeof instance.shouldComponentUpdate === 'function') { @@ -789,6 +783,8 @@ export default function( } } + resetHasForceUpdateBeforeProcessing(); + const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; @@ -806,10 +802,7 @@ export default function( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !checkHasForceUpdateAfterProcessing() ) { // 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. @@ -828,14 +821,16 @@ export default function( newState = workInProgress.memoizedState; } - const shouldUpdate = checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ); + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, + ); if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, @@ -922,6 +917,8 @@ export default function( } } + resetHasForceUpdateBeforeProcessing(); + const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); let updateQueue = workInProgress.updateQueue; @@ -940,10 +937,7 @@ export default function( oldProps === newProps && oldState === newState && !hasContextChanged() && - !( - workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate - ) + !checkHasForceUpdateAfterProcessing() ) { // 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. @@ -977,14 +971,16 @@ export default function( } } - const shouldUpdate = checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ); + const shouldUpdate = + checkHasForceUpdateAfterProcessing() || + checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + oldState, + newState, + newContext, + ); if (shouldUpdate) { // In order to support react-lifecycles-compat polyfilled components, diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 574a3c0740..dc3791c4e1 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -131,9 +131,6 @@ export type UpdateQueue = { firstCapturedEffect: Update | null, lastCapturedEffect: Update | null, - - // TODO: Workaround for lack of tuples. Could use global state instead. - hasForceUpdate: boolean, }; export const UpdateState = 0; @@ -141,6 +138,11 @@ export const ReplaceState = 1; export const ForceUpdate = 2; export const CaptureUpdate = 3; +// Global state that is reset at the beginning of calling `processUpdateQueue`. +// It should only be read right after calling `processUpdateQueue`, via +// `checkHasForceUpdateAfterProcessing`. +let hasForceUpdate = false; + let didWarnUpdateInsideUpdate; let currentlyProcessingQueue; export let resetCurrentlyProcessingQueue; @@ -164,7 +166,6 @@ export function createUpdateQueue(baseState: State): UpdateQueue { lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, - hasForceUpdate: false, }; return queue; } @@ -183,8 +184,6 @@ function cloneUpdateQueue( firstCapturedUpdate: null, lastCapturedUpdate: null, - hasForceUpdate: false, - firstEffect: null, lastEffect: null, @@ -423,7 +422,7 @@ function getStateFromUpdate( return Object.assign({}, prevState, partialState); } case ForceUpdate: { - queue.hasForceUpdate = true; + hasForceUpdate = true; return prevState; } } @@ -437,6 +436,8 @@ export function processUpdateQueue( instance: any, renderExpirationTime: ExpirationTime, ): void { + hasForceUpdate = false; + if ( queue.expirationTime === NoWork || queue.expirationTime > renderExpirationTime @@ -595,6 +596,14 @@ function callCallback(callback, context) { callback.call(context); } +export function resetHasForceUpdateBeforeProcessing() { + hasForceUpdate = false; +} + +export function checkHasForceUpdateAfterProcessing(): boolean { + return hasForceUpdate; +} + export function commitUpdateQueue( finishedWork: Fiber, finishedQueue: UpdateQueue, diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js index a8289b113d..f58228d952 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js @@ -1115,6 +1115,29 @@ describe('ReactIncremental', () => { expect(ops).toEqual(['Foo', 'Bar', 'Baz', 'Bar', 'Baz']); }); + it('should clear forceUpdate after update is flushed', () => { + let a = 0; + + class Foo extends React.PureComponent { + render() { + const msg = `A: ${a}, B: ${this.props.b}`; + ReactNoop.yield(msg); + return msg; + } + } + + const foo = React.createRef(null); + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['A: 0, B: 0']); + + a = 1; + foo.current.forceUpdate(); + expect(ReactNoop.flush()).toEqual(['A: 1, B: 0']); + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([]); + }); + xit('can call sCU while resuming a partly mounted component', () => { let ops = [];