diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index a19e719de4054..a00c065d980e8 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -221,6 +221,10 @@ describe('ReactComponentLifeCycle', () => { 'unmounted component. This is a no-op.\n\nPlease check the code for the ' + 'StatefulComponent component.', ); + + // Check deduplication + ReactTestUtils.renderIntoDocument(); + expectDev(console.error.calls.count()).toBe(1); }); it('should correctly determine if a component is mounted', () => { diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index 56bd6c63aba24..10b4b73c335c9 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -252,6 +252,9 @@ describe('ReactCompositeComponent', () => { 'component. This is a no-op.\n\nPlease check the code for the ' + 'Component component.', ); + + instance.forceUpdate(); + expectDev(console.error.calls.count()).toBe(1); }); it('should warn about `setState` on unmounted components', () => { @@ -391,6 +394,11 @@ describe('ReactCompositeComponent', () => { expect(instance).toBe(instance2); expect(renderedState).toBe(1); expect(instance2.state.value).toBe(1); + + // Test deduplication + ReactDOM.unmountComponentAtNode(container); + ReactDOM.render(, container); + expectDev(console.error.calls.count()).toBe(1); }); it('should warn about `setState` in getChildContext', () => { @@ -424,6 +432,11 @@ describe('ReactCompositeComponent', () => { expectDev(console.error.calls.argsFor(0)[0]).toBe( 'Warning: setState(...): Cannot call setState() inside getChildContext()', ); + + // Test deduplication + ReactDOM.unmountComponentAtNode(container); + ReactDOM.render(, container); + expectDev(console.error.calls.count()).toBe(1); }); it('should cleanup even if render() fatals', () => { diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js index e2e04fa70fe65..d0299ffc99f6e 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js @@ -421,6 +421,10 @@ describe('ReactCompositeComponent-state', () => { "this.state is deprecated (except inside a component's constructor). " + 'Use setState instead.', ); + + // Check deduplication + ReactDOM.render(, container); + expect(console.error.calls.count()).toEqual(1); }); it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => { diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index ffae448c2d50e..17494318f0638 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -656,8 +656,11 @@ describe('ReactDOMServer', () => { ' This usually means you called setState() outside componentWillMount() on the server.' + ' This is a no-op.\n\nPlease check the code for the Foo component.', ); + var markup = ReactDOMServer.renderToStaticMarkup(); expect(markup).toBe('
hello
'); + jest.runOnlyPendingTimers(); + expectDev(console.error.calls.count()).toBe(1); }); it('warns with a no-op when an async forceUpdate is triggered', () => { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 277eef8995943..f3f61ef7a1725 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -118,6 +118,7 @@ var didWarnDefaultChecked = false; var didWarnDefaultSelectValue = false; var didWarnDefaultTextareaValue = false; var didWarnInvalidOptionChildren = false; +var didWarnAboutNoopUpdateForComponent = {}; var valuePropNames = ['value', 'defaultValue']; var newlineEatingTags = { listing: true, @@ -181,6 +182,13 @@ function warnNoop( ) { if (__DEV__) { var constructor = publicInstance.constructor; + const componentName = + (constructor && getComponentName(constructor)) || 'ReactClass'; + const warningKey = `${componentName}.${callerName}`; + if (didWarnAboutNoopUpdateForComponent[warningKey]) { + return; + } + warning( false, '%s(...): Can only update a mounting component. ' + @@ -188,8 +196,9 @@ function warnNoop( 'This is a no-op.\n\nPlease check the code for the %s component.', callerName, callerName, - (constructor && getComponentName(constructor)) || 'ReactClass', + componentName, ); + didWarnAboutNoopUpdateForComponent[warningKey] = true; } } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 396be01fb8ab0..2a1e9a64ee3b9 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -41,6 +41,7 @@ if (__DEV__) { var warning = require('fbjs/lib/warning'); var {startPhaseTimer, stopPhaseTimer} = require('./ReactDebugFiberPerf'); + var didWarnAboutStateAssignmentForComponent = {}; var warnOnInvalidCallback = function(callback: mixed, callerName: string) { warning( @@ -393,13 +394,17 @@ module.exports = function( if (instance.state !== oldState) { if (__DEV__) { - warning( - false, - '%s.componentWillReceiveProps(): Assigning directly to ' + - "this.state is deprecated (except inside a component's " + - 'constructor). Use setState instead.', - getComponentName(workInProgress), - ); + const componentName = getComponentName(workInProgress) || 'Component'; + if (!didWarnAboutStateAssignmentForComponent[componentName]) { + warning( + false, + '%s.componentWillReceiveProps(): Assigning directly to ' + + "this.state is deprecated (except inside a component's " + + 'constructor). Use setState instead.', + componentName, + ); + didWarnAboutStateAssignmentForComponent[componentName] = true; + } } updater.enqueueReplaceState(instance, instance.state, null); } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 67a87c9a4a3f8..da3ed2bc8c002 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -101,34 +101,41 @@ if (__DEV__) { } = require('./ReactDebugFiberPerf'); var didWarnAboutStateTransition = false; + var didWarnSetStateChildContext = false; + var didWarnStateUpdateForUnmountedComponent = {}; - var warnAboutUpdateOnUnmounted = function( - instance: React$ComponentType, - ) { - const ctor = instance.constructor; + var warnAboutUpdateOnUnmounted = function(fiber: Fiber) { + const componentName = getComponentName(fiber) || 'ReactClass'; + if (didWarnStateUpdateForUnmountedComponent[componentName]) { + return; + } warning( false, - 'Can only update a mounted or mounting component. This usually means ' + - 'you called setState, replaceState, or forceUpdate on an unmounted ' + - 'component. This is a no-op.\n\nPlease check the code for the ' + - '%s component.', - (ctor && (ctor.displayName || ctor.name)) || 'ReactClass', + 'Can only update a mounted or mounting ' + + 'component. This usually means you called setState, replaceState, ' + + 'or forceUpdate on an unmounted component. This is a no-op.\n\nPlease ' + + 'check the code for the %s component.', + componentName, ); + didWarnStateUpdateForUnmountedComponent[componentName] = true; }; - var warnAboutInvalidUpdates = function(instance: React$ComponentType) { + var warnAboutInvalidUpdates = function(instance: React$Component) { switch (ReactDebugCurrentFiber.phase) { case 'getChildContext': + if (didWarnSetStateChildContext) { + return; + } warning( false, 'setState(...): Cannot call setState() inside getChildContext()', ); + didWarnSetStateChildContext = true; break; case 'render': if (didWarnAboutStateTransition) { return; } - didWarnAboutStateTransition = true; warning( false, 'Cannot update during an existing state transition (such as within ' + @@ -136,6 +143,7 @@ if (__DEV__) { 'be a pure function of props and state; constructor side-effects are ' + 'an anti-pattern, but can be moved to `componentWillMount`.', ); + didWarnAboutStateTransition = true; break; } }; @@ -1229,7 +1237,7 @@ module.exports = function( } else { if (__DEV__) { if (!isErrorRecovery && fiber.tag === ClassComponent) { - warnAboutUpdateOnUnmounted(fiber.stateNode); + warnAboutUpdateOnUnmounted(fiber); } } return; diff --git a/packages/react-reconciler/src/ReactFiberUpdateQueue.js b/packages/react-reconciler/src/ReactFiberUpdateQueue.js index 1fd6fa18cb8b2..9773bdc9e8380 100644 --- a/packages/react-reconciler/src/ReactFiberUpdateQueue.js +++ b/packages/react-reconciler/src/ReactFiberUpdateQueue.js @@ -20,6 +20,7 @@ const {NoWork} = require('./ReactFiberExpirationTime'); if (__DEV__) { var warning = require('fbjs/lib/warning'); + var didWarnUpdateInsideUpdate = false; } type PartialState = @@ -132,7 +133,10 @@ function insertUpdateIntoFiber( // Warn if an update is scheduled from inside an updater function. if (__DEV__) { - if (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) { + if ( + (queue1.isProcessing || (queue2 !== null && queue2.isProcessing)) && + !didWarnUpdateInsideUpdate + ) { warning( false, 'An update (setState, replaceState, or forceUpdate) was scheduled ' + @@ -140,6 +144,7 @@ function insertUpdateIntoFiber( 'with zero side-effects. Consider using componentDidUpdate or a ' + 'callback.', ); + didWarnUpdateInsideUpdate = true; } } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index 0d00233462f4d..b295897199593 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -341,6 +341,19 @@ describe('ReactIncrementalUpdates', () => { expect(instance.state).toEqual({a: 'a', b: 'b'}); expectDev(console.error.calls.count()).toBe(1); - console.error.calls.reset(); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'An update (setState, replaceState, or forceUpdate) was scheduled ' + + 'from inside an update function. Update functions should be pure, ' + + 'with zero side-effects. Consider using componentDidUpdate or a ' + + 'callback.', + ); + + // Test deduplication + instance.setState(function a() { + this.setState({a: 'a'}); + return {b: 'b'}; + }); + ReactNoop.flush(); + expectDev(console.error.calls.count()).toBe(1); }); }); diff --git a/packages/react/src/ReactNoopUpdateQueue.js b/packages/react/src/ReactNoopUpdateQueue.js index 5b0631dd08617..3d4308af09c1b 100644 --- a/packages/react/src/ReactNoopUpdateQueue.js +++ b/packages/react/src/ReactNoopUpdateQueue.js @@ -9,11 +9,19 @@ if (__DEV__) { var warning = require('fbjs/lib/warning'); + var didWarnStateUpdateForUnmountedComponent = {}; } function warnNoop(publicInstance, callerName) { if (__DEV__) { var constructor = publicInstance.constructor; + const componentName = + (constructor && (constructor.displayName || constructor.name)) || + 'ReactClass'; + const warningKey = `${componentName}.${callerName}`; + if (didWarnStateUpdateForUnmountedComponent[warningKey]) { + return; + } warning( false, '%s(...): Can only update a mounted or mounting component. ' + @@ -21,9 +29,9 @@ function warnNoop(publicInstance, callerName) { 'This is a no-op.\n\nPlease check the code for the %s component.', callerName, callerName, - (constructor && (constructor.displayName || constructor.name)) || - 'ReactClass', + componentName, ); + didWarnStateUpdateForUnmountedComponent[warningKey] = true; } }