diff --git a/docs/docs/12-context.md b/docs/docs/12-context.md index 1eafd089b6dc..11029c196304 100644 --- a/docs/docs/12-context.md +++ b/docs/docs/12-context.md @@ -145,6 +145,14 @@ void componentDidUpdate( ) ``` +Additionally, a new lifecycle method named `componentWillReceiveContext` is included, which is called after `componentWillReceiveProps`, but before the update occurs. This method will *always* be called if contexts are enabled, unlike `componentWillReceiveProps`, which is only called if props change. + +```javascript +void componentWillReceiveContext( + object nextContext +) +``` + ## Referencing context in stateless functional components Stateless functional components are also able to reference `context` if `contextTypes` is defined as a property of the function. The following code shows the `Button` component above written as a stateless functional component. diff --git a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js index 891cce6eff51..471b2431eb1e 100644 --- a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js +++ b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js @@ -73,6 +73,7 @@ describe('ReactContextValidator', function() { it('should filter context properly in callbacks', function() { var actualComponentWillReceiveProps; + var actualComponentWillReceiveContext; var actualShouldComponentUpdate; var actualComponentWillUpdate; var actualComponentDidUpdate; @@ -105,6 +106,11 @@ describe('ReactContextValidator', function() { return true; }, + componentWillReceiveContext: function(nextContext) { + actualComponentWillReceiveContext = nextContext; + return true; + }, + shouldComponentUpdate: function(nextProps, nextState, nextContext) { actualShouldComponentUpdate = nextContext; return true; @@ -127,6 +133,7 @@ describe('ReactContextValidator', function() { ReactDOM.render(, container); ReactDOM.render(, container); expect(actualComponentWillReceiveProps).toEqual({foo: 'def'}); + expect(actualComponentWillReceiveContext).toEqual({foo: 'def'}); expect(actualShouldComponentUpdate).toEqual({foo: 'def'}); expect(actualComponentWillUpdate).toEqual({foo: 'def'}); expect(actualComponentDidUpdate).toEqual({foo: 'abc'}); diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js index ea049acafccb..bb6bf3661754 100644 --- a/src/isomorphic/classic/class/ReactClass.js +++ b/src/isomorphic/classic/class/ReactClass.js @@ -219,6 +219,18 @@ var ReactClassInterface = { */ componentWillReceiveProps: SpecPolicy.DEFINE_MANY, + /** + * Invoked before the component receives new context. + * + * This method is analogous to `componentWillReceiveProps`, + * but will only receive the new and changed context. This method exists + * for situations where the former does not trigger, as props have not changed. + * + * @param {object} nextContext + * @optional + */ + componentWillReceiveContext: SpecPolicy.DEFINE_MANY, + /** * Invoked while deciding if the component should be updated as a result of * receiving new props, state and/or context. @@ -840,6 +852,12 @@ var ReactClass = { 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', spec.displayName || 'A component' ); + warning( + !Constructor.prototype.componentWillRecieveContext, + '%s has a method called ' + + 'componentWillRecieveContext(). Did you mean componentWillReceiveContext()?', + spec.displayName || 'A component' + ); } // Reduce time spent doing lookups by setting these on the prototype. diff --git a/src/isomorphic/classic/class/__tests__/ReactClass-test.js b/src/isomorphic/classic/class/__tests__/ReactClass-test.js index b9558f83b912..357466107ae4 100644 --- a/src/isomorphic/classic/class/__tests__/ReactClass-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactClass-test.js @@ -178,6 +178,22 @@ describe('ReactClass-spec', function() { ); }); + it('should warn when mispelling componentWillReceiveContext', function() { + React.createClass({ + componentWillRecieveContext: function() { + return false; + }, + render: function() { + return
; + }, + }); + expect(console.error.argsForCall.length).toBe(1); + expect(console.error.argsForCall[0][0]).toBe( + 'Warning: A component has a method called componentWillRecieveContext(). Did you ' + + 'mean componentWillReceiveContext()?' + ); + }); + it('should throw if a reserved property is in statics', function() { expect(function() { React.createClass({ diff --git a/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee b/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee index a712152090fe..a096fddba60b 100644 --- a/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee @@ -267,6 +267,44 @@ describe 'ReactCoffeeScriptClass', -> ReactDOM.unmountComponentAtNode container expect(lifeCycles).toEqual ['will-unmount'] + it 'will call context life cycle methods', -> + lifeCycles = [] + + class Child extends React.Component + @contextTypes: + foo: React.PropTypes.string + + componentWillReceiveProps: (nextProps, nextContext) -> + lifeCycles.push 'receive-props', nextProps, nextContext + + componentWillReceiveContext: (nextContext) -> + lifeCycles.push 'receive-context', nextContext + + render: -> + span + className: @props.value + + class Parent extends React.Component + @childContextTypes: + foo: React.PropTypes.string + + getChildContext: -> + { foo: 'foo' } + + render: -> + React.createElement('div', {}, React.createElement(Child, { value: @props.value })) + + test React.createElement(Parent, value: 'bar'), 'DIV', '' + expect(lifeCycles).toEqual [] + + test React.createElement(Parent, value: 'baz'), 'DIV', '' + expect(lifeCycles).toEqual [ + 'receive-props', { value: 'baz' }, { foo: 'foo' }, + 'receive-context', { foo: 'foo' } + ] + + ReactDOM.unmountComponentAtNode container + it 'warns when classic properties are defined on the instance, but does not invoke them.', -> spyOn console, 'error' @@ -341,6 +379,23 @@ describe 'ReactCoffeeScriptClass', -> Did you mean componentWillReceiveProps()?' ) + it 'should warn when misspelling componentWillReceiveContext', -> + spyOn console, 'error' + class NamedComponent extends React.Component + componentWillRecieveContext: -> + false + + render: -> + span + className: 'foo' + + test React.createElement(NamedComponent), 'SPAN', 'foo' + expect(console.error.calls.length).toBe 1 + expect(console.error.argsForCall[0][0]).toBe( + 'Warning: NamedComponent has a method called componentWillRecieveContext(). + Did you mean componentWillReceiveContext()?' + ) + it 'should throw AND warn when trying to access classic APIs', -> spyOn console, 'error' instance = diff --git a/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js b/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js index 3fc043bb10a1..435e3982bdc0 100644 --- a/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js @@ -276,8 +276,11 @@ describe('ReactES6Class', function() { componentDidMount() { lifeCycles.push('did-mount'); } - componentWillReceiveProps(nextProps) { - lifeCycles.push('receive-props', nextProps); + componentWillReceiveProps(nextProps, nextContext) { + lifeCycles.push('receive-props', nextProps, nextContext); + } + componentWillReceiveContext(nextContext) { + lifeCycles.push('receive-context', nextContext); } shouldComponentUpdate(nextProps, nextState) { lifeCycles.push('should-update', nextProps, nextState); @@ -304,7 +307,7 @@ describe('ReactES6Class', function() { lifeCycles = []; // reset test(, 'SPAN', 'bar'); expect(lifeCycles).toEqual([ - 'receive-props', freeze({value: 'bar'}), + 'receive-props', freeze({value: 'bar'}), {}, 'should-update', freeze({value: 'bar'}), {}, 'will-update', freeze({value: 'bar'}), {}, 'did-update', freeze({value: 'foo'}), {}, @@ -316,6 +319,52 @@ describe('ReactES6Class', function() { ]); }); + it('will call context life cycle methods', function() { + var lifeCycles = []; + + class Child extends React.Component { + componentWillReceiveProps(nextProps, nextContext) { + lifeCycles.push('receive-props', nextProps, nextContext); + } + componentWillReceiveContext(nextContext) { + lifeCycles.push('receive-context', nextContext); + } + render() { + return ; + } + } + Child.contextTypes = { + foo: React.PropTypes.string, + }; + + class Parent extends React.Component { + getChildContext() { + return { + foo: 'foo', + }; + } + render() { + return ( +
+ ); + } + } + Parent.childContextTypes = { + foo: React.PropTypes.string, + }; + + test(, 'DIV', ''); + expect(lifeCycles).toEqual([]); + + test(, 'DIV', ''); + expect(lifeCycles).toEqual([ + 'receive-props', freeze({value: 'baz'}), freeze({foo: 'foo'}), + 'receive-context', freeze({foo: 'foo'}), + ]); + + ReactDOM.unmountComponentAtNode(container); + }); + it('warns when classic properties are defined on the instance, but does not invoke them.', function() { spyOn(console, 'error'); var getDefaultPropsWasCalled = false; @@ -399,6 +448,27 @@ describe('ReactES6Class', function() { ); }); + it('should warn when misspelling componentWillReceiveContext', function() { + spyOn(console, 'error'); + + class NamedComponent extends React.Component { + componentWillRecieveContext() { + return false; + } + render() { + return ; + } + } + test(, 'SPAN', 'foo'); + + expect(console.error.calls.length).toBe(1); + expect(console.error.argsForCall[0][0]).toBe( + 'Warning: ' + + 'NamedComponent has a method called componentWillRecieveContext(). Did ' + + 'you mean componentWillReceiveContext()?' + ); + }); + it('should throw AND warn when trying to access classic APIs', function() { spyOn(console, 'error'); var instance = test(, 'DIV', 'foo'); diff --git a/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts b/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts index 0b89ed260c0d..b247fcdfa639 100644 --- a/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts +++ b/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts @@ -233,6 +233,34 @@ class NormalLifeCycles extends React.Component { } } +// will call context life cycle methods +class ContextChildLifeCycles extends React.Component { + props : any; + state = {}; + static contextTypes = { foo: React.PropTypes.string }; + componentWillReceiveProps(nextProps, nextContext) { + lifeCycles.push('receive-props', nextProps, nextContext); + } + componentWillReceiveContext(nextContext) { + lifeCycles.push('receive-context', nextContext); + } + render() { + return React.createElement('span', {className: this.props.value}); + } +} + +class ContextParentLifeCycles extends React.Component { + static childContextTypes = { foo: React.PropTypes.string }; + getChildContext() { + return { + foo: 'foo' + }; + } + render() { + return React.createElement('div', {}, React.createElement(ContextChildLifeCycles, { value: this.props.value })); + } +} + // warns when classic properties are defined on the instance, // but does not invoke them. var getInitialStateWasCalled = false; @@ -273,6 +301,16 @@ class MisspelledComponent2 extends React.Component { } } +// it should warn when misspelling componentWillReceiveContext +class MisspelledComponent3 extends React.Component { + componentWillRecieveContext() { + return false; + } + render() { + return React.createElement('span', {className: 'foo'}); + } +} + // it supports this.context passed via getChildContext class ReadContext extends React.Component { static contextTypes = { bar: React.PropTypes.string }; @@ -428,6 +466,20 @@ describe('ReactTypeScriptClass', function() { ]); }); + it('will call context life cycle methods', function() { + lifeCycles = []; + test(React.createElement(ContextParentLifeCycles, {value: 'bar'}), 'DIV', ''); + expect(lifeCycles).toEqual([]); + + test(React.createElement(ContextParentLifeCycles, {value: 'baz'}), 'DIV', ''); + expect(lifeCycles).toEqual([ + 'receive-props', {value: 'baz'}, {foo: 'foo'}, + 'receive-context', {foo: 'foo'} + ]); + + ReactDOM.unmountComponentAtNode(container); + }); + it('warns when classic properties are defined on the instance, ' + 'but does not invoke them.', function() { spyOn(console, 'error'); @@ -480,6 +532,19 @@ describe('ReactTypeScriptClass', function() { ); }); + it('should warn when misspelling componentWillReceiveContext', function() { + spyOn(console, 'error'); + + test(React.createElement(MisspelledComponent3), 'SPAN', 'foo'); + + expect((console.error).argsForCall.length).toBe(1); + expect((console.error).argsForCall[0][0]).toBe( + 'Warning: ' + + 'MisspelledComponent3 has a method called componentWillRecieveContext(). ' + + 'Did you mean componentWillReceiveContext()?' + ); + }); + it('should throw AND warn when trying to access classic APIs', function() { spyOn(console, 'error'); var instance = test( diff --git a/src/renderers/dom/server/__tests__/ReactServerRendering-test.js b/src/renderers/dom/server/__tests__/ReactServerRendering-test.js index 10c683d42396..29e3c1cc6506 100644 --- a/src/renderers/dom/server/__tests__/ReactServerRendering-test.js +++ b/src/renderers/dom/server/__tests__/ReactServerRendering-test.js @@ -140,6 +140,9 @@ describe('ReactServerRendering', function() { componentWillReceiveProps: function() { lifecycle.push('componentWillReceiveProps'); }, + componentWillReceiveContext: function() { + lifecycle.push('componentWillReceiveContext'); + }, componentWillUnmount: function() { lifecycle.push('componentWillUnmount'); }, @@ -331,6 +334,9 @@ describe('ReactServerRendering', function() { componentWillReceiveProps: function() { lifecycle.push('componentWillReceiveProps'); }, + componentWillReceiveContext: function() { + lifecycle.push('componentWillReceiveContext'); + }, componentWillUnmount: function() { lifecycle.push('componentWillUnmount'); }, diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index 3c5da9815744..e8392fe77c36 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -59,6 +59,7 @@ StatelessComponent.prototype.render = function() { * * Update Phases: * - componentWillReceiveProps (only called if parent updated) + * - componentWillReceiveContext (only if there is a context) * - shouldComponentUpdate * - componentWillUpdate * - render @@ -272,6 +273,12 @@ var ReactCompositeComponentMixin = { 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', (this.getName() || 'A component') ); + warning( + typeof inst.componentWillRecieveContext !== 'function', + '%s has a method called ' + + 'componentWillRecieveContext(). Did you mean componentWillReceiveContext()?', + (this.getName() || 'A component') + ); } var initialState = inst.state; @@ -625,10 +632,10 @@ var ReactCompositeComponentMixin = { }, /** - * Perform an update to a mounted component. The componentWillReceiveProps and - * shouldComponentUpdate methods are called, then (assuming the update isn't - * skipped) the remaining update lifecycle methods are called and the DOM - * representation is updated. + * Perform an update to a mounted component. The `componentWillReceiveProps`, + * `componentWillReceiveContext`, and `shouldComponentUpdate` methods are called, + * then (assuming the update isn't skipped) the remaining update lifecycle methods + * are called and the DOM representation is updated. * * By default, this implements React's rendering and reconciliation algorithm. * Sophisticated clients may wish to override this. @@ -669,6 +676,11 @@ var ReactCompositeComponentMixin = { } } + // Only trigger if the context is not an empty object + if (nextContext !== emptyObject && inst.componentWillReceiveContext) { + inst.componentWillReceiveContext(nextContext); + } + var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = diff --git a/src/renderers/shared/reconciler/__tests__/ReactComponentLifeCycle-test.js b/src/renderers/shared/reconciler/__tests__/ReactComponentLifeCycle-test.js index 3b938fa06e68..7220ce6ca908 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactComponentLifeCycle-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactComponentLifeCycle-test.js @@ -524,31 +524,41 @@ describe('ReactComponentLifeCycle', function() { }; }; var Outer = React.createClass({ + childContextTypes: { + foo: React.PropTypes.string, + }, + getChildContext() { + return { foo: 'bar' }; + }, render: function() { return
; }, componentWillMount: logger('outer componentWillMount'), componentDidMount: logger('outer componentDidMount'), componentWillReceiveProps: logger('outer componentWillReceiveProps'), + componentWillReceiveContext: logger('outer componentWillReceiveContext'), shouldComponentUpdate: logger('outer shouldComponentUpdate'), componentWillUpdate: logger('outer componentWillUpdate'), componentDidUpdate: logger('outer componentDidUpdate'), componentWillUnmount: logger('outer componentWillUnmount'), }); var Inner = React.createClass({ + contextTypes: { + foo: React.PropTypes.string, + }, render: function() { return {this.props.x}; }, componentWillMount: logger('inner componentWillMount'), componentDidMount: logger('inner componentDidMount'), componentWillReceiveProps: logger('inner componentWillReceiveProps'), + componentWillReceiveContext: logger('inner componentWillReceiveContext'), shouldComponentUpdate: logger('inner shouldComponentUpdate'), componentWillUpdate: logger('inner componentWillUpdate'), componentDidUpdate: logger('inner componentDidUpdate'), componentWillUnmount: logger('inner componentWillUnmount'), }); - var container = document.createElement('div'); log = []; ReactDOM.render(, container); @@ -566,6 +576,7 @@ describe('ReactComponentLifeCycle', function() { 'outer shouldComponentUpdate', 'outer componentWillUpdate', 'inner componentWillReceiveProps', + 'inner componentWillReceiveContext', 'inner shouldComponentUpdate', 'inner componentWillUpdate', 'inner componentDidUpdate', diff --git a/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js b/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js index 17f8ba963af6..711570edfbce 100644 --- a/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js +++ b/src/renderers/shared/reconciler/__tests__/ReactCompositeComponent-test.js @@ -812,6 +812,10 @@ describe('ReactCompositeComponent', function() { expect('foo' in nextContext).toBe(true); }, + componentWillReceiveContext: function(nextContext) { + expect('foo' in nextContext).toBe(true); + }, + componentDidUpdate: function(prevProps, prevState, prevContext) { expect('foo' in prevContext).toBe(true); }, @@ -832,6 +836,10 @@ describe('ReactCompositeComponent', function() { expect('foo' in nextContext).toBe(false); }, + componentWillReceiveContext: function(nextContext) { + expect('foo' in nextContext).toBe(false); + }, + componentDidUpdate: function(prevProps, prevState, prevContext) { expect('foo' in prevContext).toBe(false); }, @@ -897,6 +905,68 @@ describe('ReactCompositeComponent', function() { ); }); + it('should not call componentWillReceiveContext for the defining component', function() { + var notTriggered = true; + + var Child = React.createClass({ + contextTypes: { + foo: React.PropTypes.string, + }, + render() { + return
; + }, + }); + + var Parent = React.createClass({ + childContextTypes: { + foo: React.PropTypes.string, + }, + getChildContext() { + return { foo: 'bar' }; + }, + componentWillReceiveContext() { + notTriggered = false; + }, + render() { + return ; + }, + }); + + ReactDOM.render(, document.createElement('div')); + expect(notTriggered).toBe(true); + }); + + it('should not call componentWillReceiveContext if the context is an empty object', function() { + var notTriggered = true; + + var Child = React.createClass({ + contextTypes: { + foo: React.PropTypes.string, + }, + componentWillReceiveContext() { + notTriggered = false; + }, + render() { + return
; + }, + }); + + var Parent = React.createClass({ + childContextTypes: { + foo: React.PropTypes.string, + }, + getChildContext() { + return { }; + }, + render() { + return ; + }, + }); + + ReactDOM.render(, document.createElement('div')); + expect(notTriggered).toBe(true); + }); + it('only renders once if updated in componentWillReceiveProps', function() { var renders = 0; var Component = React.createClass({ @@ -922,6 +992,49 @@ describe('ReactCompositeComponent', function() { expect(instance.state.updated).toBe(true); }); + it('only renders once if updated in componentWillReceiveContext', function() { + var renders = 0; + var Child = React.createClass({ + contextTypes: { + update: React.PropTypes.number, + }, + getInitialState: function() { + return {updated: false}; + }, + componentWillReceiveContext: function(context) { + expect(context.update).toBe(1); + this.setState({updated: Boolean(context.update) }); + }, + render: function() { + renders++; + return
; + }, + }); + + var Parent = React.createClass({ + childContextTypes: { + update: React.PropTypes.number, + }, + getDefaultProps() { + return { update: 0 }; + }, + getChildContext() { + return { update: this.props.update }; + }, + render() { + return ; + }, + }); + + var container = document.createElement('div'); + var instance = ReactDOM.render(, container); + expect(renders).toBe(1); + expect(instance.refs.child.state.updated).toBe(false); + ReactDOM.render(, container); + expect(renders).toBe(2); + expect(instance.refs.child.state.updated).toBe(true); + }); + it('should update refs if shouldComponentUpdate gives false', function() { var Static = React.createClass({ shouldComponentUpdate: function() {