diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 072fe059889..faa85d2c130 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -607,6 +607,7 @@ src/renderers/__tests__/ReactCompositeComponentState-test.js * should batch unmounts * should update state when called from child cWRP * should merge state when sCU returns false +* should treat assigning to this.state inside cWRP as a replaceState, with a warning src/renderers/__tests__/ReactEmptyComponent-test.js * should not produce child DOM nodes for null and false diff --git a/src/renderers/__tests__/ReactCompositeComponentState-test.js b/src/renderers/__tests__/ReactCompositeComponentState-test.js index 90189534384..46ec0f1fe45 100644 --- a/src/renderers/__tests__/ReactCompositeComponentState-test.js +++ b/src/renderers/__tests__/ReactCompositeComponentState-test.js @@ -414,4 +414,43 @@ describe('ReactCompositeComponent-state', () => { 'scu from a,b to a,b,c', ]); }); + + it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', () => { + spyOn(console, 'error'); + + let ops = []; + class Test extends React.Component { + state = { step: 1, extra: true }; + componentWillReceiveProps() { + this.setState({ step: 2 }, () => { + // Tests that earlier setState callbacks are not dropped + ops.push(`callback -- step: ${this.state.step}, extra: ${!!this.state.extra}`); + }); + // Treat like replaceState + this.state = { step: 3 }; + } + render() { + ops.push(`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`); + return null; + } + } + + // Mount + const container = document.createElement('div'); + ReactDOM.render(, container); + // Update + ReactDOM.render(, container); + + expect(ops).toEqual([ + 'render -- step: 1, extra: true', + 'render -- step: 3, extra: false', + 'callback -- step: 3, extra: false', + ]); + expect(console.error.calls.count()).toEqual(1); + expect(console.error.calls.argsFor(0)[0]).toEqual( + 'Warning: Test.componentWillReceiveProps(): Assigning directly to ' + + 'this.state is deprecated (except inside a component\'s constructor). ' + + 'Use setState instead.' + ); + }); }); diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 87f3426099e..eae8a56a21e 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -408,6 +408,19 @@ module.exports = function( if (oldProps !== newProps || oldContext !== newContext) { if (typeof instance.componentWillReceiveProps === 'function') { instance.componentWillReceiveProps(newProps, newContext); + + if (instance.state !== workInProgress.memoizedState) { + if (__DEV__) { + warning( + false, + '%s.componentWillReceiveProps(): Assigning directly to ' + + 'this.state is deprecated (except inside a component\'s ' + + 'constructor). Use setState instead.', + getComponentName(workInProgress) + ); + } + updater.enqueueReplaceState(instance, instance.state, null); + } } } diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js index f534cfd5c71..9a572a8f6f0 100644 --- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js @@ -850,6 +850,7 @@ var ReactCompositeComponent = { // _pendingStateQueue which will ensure that any state updates gets // immediately reconciled instead of waiting for the next batch. if (willReceive && inst.componentWillReceiveProps) { + const beforeState = inst.state; if (__DEV__) { measureLifeCyclePerf( () => inst.componentWillReceiveProps(nextProps, nextContext), @@ -859,6 +860,20 @@ var ReactCompositeComponent = { } else { inst.componentWillReceiveProps(nextProps, nextContext); } + const afterState = inst.state; + if (beforeState !== afterState) { + inst.state = beforeState; + inst.updater.enqueueReplaceState(inst, afterState); + if (__DEV__) { + warning( + false, + '%s.componentWillReceiveProps(): Assigning directly to ' + + 'this.state is deprecated (except inside a component\'s ' + + 'constructor). Use setState instead.', + this.getName() || 'ReactCompositeComponent' + ); + } + } } // If updating happens to enqueue any new updates, we shouldn't execute new