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() {