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