Summary:
adds `this.context` which you can think of as implicit props, which are passed automatically down the //ownership// hierarchy.

Contexts should be used sparingly, since they essentially allow components to communicate with descendants (in the ownership sense, not parenthood sense), which is not usually a good idea. You probably would only use contexts in places where you'd normally use a global, but contexts allow you to override them for certain view subtrees which you can't do with globals.

The context starts out `null`:

  var RootComponent = React.createClass({
    render: function() {
      // this.context === null
    }
  });

You should **never** mutate the context directly, just like props and state.

You can change the context of your children (the ones you own, not `this.props.children` or via other props) using the new `withContext` method on `React`:

  var RootComponent = React.createClass({
    render: function() {
      // this.context === null
      var children = React.withContext({foo: 'a', bar: 'b'}, () => (
        // In ChildComponent#render, this.context === {foo: 'a', bar: 'b'}
        <ChildComponent />
      ));
      // this.context === null
    }
  });

Contexts are merged, so a component can override its owner's context **for its children**:

  var ChildComponent = React.createClass({
    render: function() {
      // this.context === {foo: 'a', bar: 'b'} (for the caller above)
      var children = React.withContext({foo: 'c'},() => (
        // In GrandchildComponent#render,
        // this.context === {foo: 'c', bar: 'b'}
        <GrandchildComponent />
      ));
      // this.context === {foo: 'a', bar: 'b'}
    }
  });
This commit is contained in:
Marshall Roch
2013-11-18 10:56:24 -08:00
committed by Paul O’Shannessy
parent 7df127db31
commit b91396be8e
11 changed files with 455 additions and 30 deletions
@@ -138,7 +138,7 @@ var ReactTransitionableChild = React.createClass({
}
},
componentDidUpdate: function(prevProps, prevState, node) {
componentDidUpdate: function(prevProps, prevState, prevContext, node) {
if (prevProps.children && !this.props.children) {
this.transition('leave', true, this.props.onDoneLeaving);
}
+2
View File
@@ -20,6 +20,7 @@
var ReactComponent = require('ReactComponent');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactDOM = require('ReactDOM');
var ReactDOMComponent = require('ReactDOMComponent');
@@ -53,6 +54,7 @@ var React = {
unmountAndReleaseReactRootNode: ReactMount.unmountAndReleaseReactRootNode,
isValidClass: ReactCompositeComponent.isValidClass,
isValidComponent: ReactComponent.isValidComponent,
withContext: ReactContext.withContext,
__internals: {
Component: ReactComponent,
CurrentOwner: ReactCurrentOwner,
+157 -17
View File
@@ -19,11 +19,13 @@
"use strict";
var ReactComponent = require('ReactComponent');
var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactErrorUtils = require('ReactErrorUtils');
var ReactOwner = require('ReactOwner');
var ReactPerf = require('ReactPerf');
var ReactPropTransferer = require('ReactPropTransferer');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactUpdates = require('ReactUpdates');
var invariant = require('invariant');
@@ -98,6 +100,21 @@ var ReactCompositeComponentInterface = {
*/
propTypes: SpecPolicy.DEFINE_MANY_MERGED,
/**
* Definition of context types for this component.
*
* @type {object}
* @optional
*/
contextTypes: SpecPolicy.DEFINE_MANY_MERGED,
/**
* Definition of context types this component sets for its children.
*
* @type {object}
* @optional
*/
childContextTypes: SpecPolicy.DEFINE_MANY_MERGED,
// ==== Definition methods ====
@@ -130,6 +147,12 @@ var ReactCompositeComponentInterface = {
*/
getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
/**
* @return {object}
* @optional
*/
getChildContext: SpecPolicy.DEFINE_MANY_MERGED,
/**
* Uses props from `this.props` and state from `this.state` to render the
* structure of the component.
@@ -214,7 +237,8 @@ var ReactCompositeComponentInterface = {
/**
* Invoked when the component is about to update due to a transition from
* `this.props` and `this.state` to `nextProps` and `nextState`.
* `this.props`, `this.state` and `this.context` to `nextProps`, `nextState`
* and `nextContext`.
*
* Use this as an opportunity to perform preparation before an update occurs.
*
@@ -222,6 +246,7 @@ var ReactCompositeComponentInterface = {
*
* @param {object} nextProps
* @param {?object} nextState
* @param {?object} nextContext
* @param {ReactReconcileTransaction} transaction
* @optional
*/
@@ -235,6 +260,7 @@ var ReactCompositeComponentInterface = {
*
* @param {object} prevProps
* @param {?object} prevState
* @param {?object} prevContext
* @param {DOMElement} rootNode DOM element representing the component.
* @optional
*/
@@ -288,6 +314,12 @@ var RESERVED_SPEC_KEYS = {
}
}
},
childContextTypes: function(Constructor, childContextTypes) {
Constructor.childContextTypes = childContextTypes;
},
contextTypes: function(Constructor, contextTypes) {
Constructor.contextTypes = contextTypes;
},
propTypes: function(Constructor, propTypes) {
Constructor.propTypes = propTypes;
}
@@ -512,8 +544,14 @@ var ReactCompositeComponentMixin = {
construct: function(initialProps, children) {
// Children can be either an array or more than one argument
ReactComponent.Mixin.construct.apply(this, arguments);
this.state = null;
this._pendingState = null;
this.context = this._processContext(ReactContext.current);
this._currentContext = ReactContext.current;
this._pendingContext = null;
this._compositeLifeCycleState = null;
},
@@ -659,6 +697,64 @@ var ReactCompositeComponentMixin = {
ReactUpdates.enqueueUpdate(this, callback);
},
/**
* Filters the context object to only contain keys specified in
* `contextTypes`, and asserts that they are valid.
*
* @param {object} context
* @return {?object}
* @private
*/
_processContext: function(context) {
var maskedContext = null;
var contextTypes = this.constructor.contextTypes;
if (contextTypes) {
maskedContext = {};
for (var contextName in contextTypes) {
maskedContext[contextName] = context[contextName];
}
this._checkPropTypes(
contextTypes,
maskedContext,
ReactPropTypeLocations.context
);
}
return maskedContext;
},
/**
* @param {object} currentContext
* @return {object}
* @private
*/
_processChildContext: function(currentContext) {
var childContext = this.getChildContext && this.getChildContext();
var displayName = this.constructor.displayName || 'ReactCompositeComponent';
if (childContext) {
invariant(
typeof this.constructor.childContextTypes === 'object',
'%s.getChildContext(): childContextTypes must be defined in order to ' +
'use getChildContext().',
displayName
);
this._checkPropTypes(
this.constructor.childContextTypes,
childContext,
ReactPropTypeLocations.childContext
);
for (var name in childContext) {
invariant(
name in this.constructor.childContextTypes,
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',
displayName,
name
);
}
return merge(currentContext, childContext);
}
return currentContext;
},
/**
* Processes props by setting default values for unspecified props and
* asserting that the props are valid.
@@ -667,21 +763,32 @@ var ReactCompositeComponentMixin = {
* @private
*/
_processProps: function(props) {
var propName;
var defaultProps = this._defaultProps;
for (propName in defaultProps) {
for (var propName in defaultProps) {
if (typeof props[propName] === 'undefined') {
props[propName] = defaultProps[propName];
}
}
var propTypes = this.constructor.propTypes;
if (propTypes) {
var componentName = this.constructor.displayName;
for (propName in propTypes) {
var checkProp = propTypes[propName];
if (checkProp) {
checkProp(props, propName, componentName);
}
this._checkPropTypes(propTypes, props, ReactPropTypeLocations.prop);
}
},
/**
* Assert that the props are valid
*
* @param {object} propTypes Map of prop name to a ReactPropType
* @param {object} props
* @param {string} location e.g. "prop", "context", "child context"
* @private
*/
_checkPropTypes: function(propTypes, props, location) {
var componentName = this.constructor.displayName;
for (var propName in propTypes) {
var checkProp = propTypes[propName];
if (checkProp) {
checkProp(props, propName, componentName, location);
}
}
},
@@ -707,6 +814,7 @@ var ReactCompositeComponentMixin = {
_performUpdateIfNecessary: function(transaction) {
if (this._pendingProps == null &&
this._pendingState == null &&
this._pendingContext == null &&
!this._pendingForceUpdate) {
return;
}
@@ -728,17 +836,26 @@ var ReactCompositeComponentMixin = {
var nextState = this._pendingState || this.state;
this._pendingState = null;
var nextContext = this._pendingContext || this._currentContext;
this._pendingContext = null;
if (this._pendingForceUpdate ||
!this.shouldComponentUpdate ||
this.shouldComponentUpdate(nextProps, nextState)) {
this.shouldComponentUpdate(nextProps, nextState, nextContext)) {
this._pendingForceUpdate = false;
// Will set `this.props` and `this.state`.
this._performComponentUpdate(nextProps, nextState, transaction);
// Will set `this.props`, `this.state` and `this.context`.
this._performComponentUpdate(
nextProps,
nextState,
nextContext,
transaction
);
} else {
// If it's determined that a component should not update, we still want
// to set props and state.
this.props = nextProps;
this.state = nextState;
this.context = nextContext;
}
this._compositeLifeCycleState = null;
@@ -750,30 +867,49 @@ var ReactCompositeComponentMixin = {
*
* @param {object} nextProps Next object to set as properties.
* @param {?object} nextState Next object to set as state.
* @param {?object} nextContext Next object to set as context.
* @param {ReactReconcileTransaction} transaction
* @private
*/
_performComponentUpdate: function(nextProps, nextState, transaction) {
_performComponentUpdate: function(
nextProps,
nextState,
nextContext,
transaction
) {
var prevProps = this.props;
var prevState = this.state;
var prevContext = this.context;
if (this.componentWillUpdate) {
this.componentWillUpdate(nextProps, nextState);
this.componentWillUpdate(nextProps, nextState, nextContext);
}
this.props = nextProps;
this.state = nextState;
this.updateComponent(transaction, prevProps, prevState);
this._currentContext = nextContext;
this.context = this._processContext(nextContext);
this.updateComponent(transaction, prevProps, prevState, prevContext);
if (this.componentDidUpdate) {
transaction.getReactMountReady().enqueue(
this,
this.componentDidUpdate.bind(this, prevProps, prevState)
this.componentDidUpdate.bind(this, prevProps, prevState, prevContext)
);
}
},
receiveComponent: function(nextComponent, transaction) {
this._pendingContext = nextComponent._currentContext;
ReactComponent.Mixin.receiveComponent.call(
this,
nextComponent,
transaction
);
},
/**
* Updates the component's currently mounted DOM representation.
*
@@ -783,13 +919,14 @@ var ReactCompositeComponentMixin = {
* @param {ReactReconcileTransaction} transaction
* @param {object} prevProps
* @param {?object} prevState
* @param {?object} prevContext
* @internal
* @overridable
*/
updateComponent: ReactPerf.measure(
'ReactCompositeComponent',
'updateComponent',
function(transaction, prevProps, prevState) {
function(transaction, prevProps, prevState, prevContext) {
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
var prevComponent = this._renderedComponent;
var nextComponent = this._renderValidatedComponent();
@@ -851,6 +988,8 @@ var ReactCompositeComponentMixin = {
*/
_renderValidatedComponent: function() {
var renderedComponent;
var previousContext = ReactContext.current;
ReactContext.current = this._processChildContext(this._currentContext);
ReactCurrentOwner.current = this;
try {
renderedComponent = this.render();
@@ -858,6 +997,7 @@ var ReactCompositeComponentMixin = {
// IE8 requires `catch` in order to use `finally`.
throw error;
} finally {
ReactContext.current = previousContext;
ReactCurrentOwner.current = null;
}
invariant(
+70
View File
@@ -0,0 +1,70 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactContext
*/
"use strict";
var merge = require('merge');
/**
* Keeps track of the current context.
*
* The context is automatically passed down the component ownership hierarchy
* and is accessible via `this.context` on ReactCompositeComponents.
*/
var ReactContext = {
/**
* @internal
* @type {object}
*/
current: {},
/**
* Temporarily extends the current context while executing scopedCallback.
*
* A typical use case might look like
*
* render: function() {
* var children = ReactContext.withContext({foo: 'foo'} () => (
*
* ));
* return <div>{children}</div>;
* }
*
* @param {object} newContext New context to merge into the existing context
* @param {function} scopedCallback Callback to run with the new context
* @return {ReactComponent|array<ReactComponent>}
*/
withContext: function(newContext, scopedCallback) {
var result;
var previousContext = ReactContext.current;
ReactContext.current = merge(previousContext, newContext);
try {
result = scopedCallback();
} catch (error) {
// IE8 requires `catch` in order to use `finally`.
throw error;
} finally {
ReactContext.current = previousContext;
}
return result;
}
};
module.exports = ReactContext;
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactPropTypeLocations
*/
"use strict";
var keyMirror = require('keyMirror');
var ReactPropTypeLocations = keyMirror({
props: null,
context: null,
childContext: null
});
module.exports = ReactPropTypeLocations;
+27 -9
View File
@@ -18,6 +18,8 @@
"use strict";
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var createObjectFrom = require('createObjectFrom');
var invariant = require('invariant');
@@ -87,14 +89,18 @@ var Props = {
var ANONYMOUS = '<<anonymous>>';
function createPrimitiveTypeChecker(expectedType) {
function validatePrimitiveType(propValue, propName, componentName) {
function validatePrimitiveType(propValue, propName, componentName, location) {
var propType = typeof propValue;
if (propType === 'object' && Array.isArray(propValue)) {
propType = 'array';
}
invariant(
propType === expectedType,
'Invalid prop `%s` of type `%s` supplied to `%s`, expected `%s`.',
'Invalid %s `%s` of type `%s` supplied to `%s`, expected `%s`.',
(location === ReactPropTypeLocations.prop ? 'prop' :
(location === ReactPropTypeLocations.context ? 'context' :
(location === ReactPropTypeLocations.childContext ? 'child context' :
'<unknown location>'))),
propName,
propType,
componentName,
@@ -106,10 +112,14 @@ function createPrimitiveTypeChecker(expectedType) {
function createEnumTypeChecker(expectedValues) {
var expectedEnum = createObjectFrom(expectedValues);
function validateEnumType(propValue, propName, componentName) {
function validateEnumType(propValue, propName, componentName, location) {
invariant(
expectedEnum[propValue],
'Invalid prop `%s` supplied to `%s`, expected one of %s.',
'Invalid %s `%s` supplied to `%s`, expected one of %s.',
(location === ReactPropTypeLocations.prop ? 'prop' :
(location === ReactPropTypeLocations.context ? 'context' :
(location === ReactPropTypeLocations.childContext ? 'child context' :
'<unknown location>'))),
propName,
componentName,
JSON.stringify(Object.keys(expectedEnum))
@@ -119,10 +129,14 @@ function createEnumTypeChecker(expectedValues) {
}
function createInstanceTypeChecker(expectedClass) {
function validateInstanceType(propValue, propName, componentName) {
function validateInstanceType(propValue, propName, componentName, location) {
invariant(
propValue instanceof expectedClass,
'Invalid prop `%s` supplied to `%s`, expected instance of `%s`.',
'Invalid %s `%s` supplied to `%s`, expected instance of `%s`.',
(location === ReactPropTypeLocations.prop ? 'prop' :
(location === ReactPropTypeLocations.context ? 'context' :
(location === ReactPropTypeLocations.childContext ? 'child context' :
'<unknown location>'))),
propName,
componentName,
expectedClass.name || ANONYMOUS
@@ -133,15 +147,19 @@ function createInstanceTypeChecker(expectedClass) {
function createChainableTypeChecker(validate) {
function createTypeChecker(isRequired) {
function checkType(props, propName, componentName) {
function checkType(props, propName, componentName, location) {
var propValue = props[propName];
if (propValue != null) {
// Only validate if there is a value to check.
validate(propValue, propName, componentName || ANONYMOUS);
validate(propValue, propName, componentName || ANONYMOUS, location);
} else {
invariant(
!isRequired,
'Required prop `%s` was not specified in `%s`.',
'Required %s `%s` was not specified in `%s`.',
(location === ReactPropTypeLocations.prop ?
'prop' : (location === ReactPropTypeLocations.context ?
'context' : (location === ReactPropTypeLocations.childContext ?
'child context' : '<unknown location>'))),
propName,
componentName || ANONYMOUS
);
@@ -476,4 +476,147 @@ describe('ReactCompositeComponent', function() {
console.warn = warn;
}
});
it('should pass context', function() {
var childInstance = null;
var grandchildInstance = null;
var Parent = React.createClass({
childContextTypes: {
foo: ReactPropTypes.string,
depth: ReactPropTypes.number
},
getChildContext: function() {
return {
foo: 'bar',
depth: 0
};
},
render: function() {
childInstance = <Child />;
return childInstance;
}
});
var Child = React.createClass({
contextTypes: {
foo: ReactPropTypes.string,
depth: ReactPropTypes.number
},
childContextTypes: {
depth: ReactPropTypes.number
},
getChildContext: function() {
return {
depth: this.context.depth + 1
};
},
render: function() {
grandchildInstance = <Grandchild />;
return grandchildInstance;
}
});
var Grandchild = React.createClass({
contextTypes: {
foo: ReactPropTypes.string,
depth: ReactPropTypes.number
},
render: function() {
return <div />;
}
});
var instance = <Parent />;
ReactTestUtils.renderIntoDocument(instance);
reactComponentExpect(childInstance).scalarContextEqual({foo: 'bar', depth: 0});
reactComponentExpect(grandchildInstance).scalarContextEqual({foo: 'bar', depth: 1});
});
it('should check context types', function() {
var Component = React.createClass({
contextTypes: {
foo: ReactPropTypes.string.isRequired
},
render: function() {
return <div />;
}
});
expect(function() {
ReactTestUtils.renderIntoDocument(<Component />);
}).toThrow(
'Invariant Violation: Required context `foo` was not specified in ' +
'`Component`.'
);
expect(function() {
React.withContext({foo: 'bar'}, function() {
ReactTestUtils.renderIntoDocument(<Component />);
});
}).not.toThrow();
expect(function() {
React.withContext({foo: 123}, function() {
ReactTestUtils.renderIntoDocument(<Component />);
});
}).toThrow(
'Invariant Violation: Invalid context `foo` of type `number` supplied ' +
'to `Component`, expected `string`.'
);
});
it('should check child context types', function() {
var Component = React.createClass({
childContextTypes: {
foo: ReactPropTypes.string.isRequired,
bar: ReactPropTypes.number
},
getChildContext: function() {
return this.props.testContext;
},
render: function() {
return <div />;
}
});
expect(function() {
ReactTestUtils.renderIntoDocument(
<Component testContext={{bar: 123}} />
);
}).toThrow(
'Invariant Violation: Required child context `foo` was not specified ' +
'in `Component`.'
);
expect(function() {
ReactTestUtils.renderIntoDocument(
<Component testContext={{foo: 123}} />
);
}).toThrow(
'Invariant Violation: Invalid child context `foo` of type `number` ' +
'supplied to `Component`, expected `string`.'
);
expect(function() {
ReactTestUtils.renderIntoDocument(
<Component testContext={{foo: 'foo', bar: 123}} />
);
}).not.toThrow();
expect(function() {
ReactTestUtils.renderIntoDocument(
<Component testContext={{foo: 'foo'}} />
);
}).not.toThrow();
});
});
+8 -1
View File
@@ -20,13 +20,20 @@
"use strict";
var Props = require('ReactPropTypes');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
function typeCheck(declaration, value) {
var props = {};
if (arguments.length > 1) {
props.testProp = value;
}
return declaration.bind(null, props, 'testProp', 'testComponent');
return declaration.bind(
null,
props,
'testProp',
'testComponent',
ReactPropTypeLocations.prop
);
}
describe('Primitive Types', function() {
+1 -1
View File
@@ -92,7 +92,7 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
delete instancesByReactID[id];
},
componentDidUpdate: function(prevProps, prevState, rootNode) {
componentDidUpdate: function(prevProps, prevState, prevContext, rootNode) {
if (this.props.checked != null) {
DOMPropertyOperations.setValueForProperty(
rootNode,
+1 -1
View File
@@ -110,7 +110,7 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
return textarea(props, this.state.initialValue);
},
componentDidUpdate: function(prevProps, prevState, rootNode) {
componentDidUpdate: function(prevProps, prevState, prevContext, rootNode) {
var value = this.getValue();
if (value != null) {
// Cast `value` to a string to ensure the value is set correctly. While
+16
View File
@@ -196,6 +196,22 @@ mergeInto(reactComponentExpect.prototype, {
.toEqual(propNameToExpectedValue[propName]);
}
return this;
},
/**
* Check a set of props are equal to a set of expected values - only works
* with scalars.
*/
scalarContextEqual: function(contextNameToExpectedValue) {
expect(this.instance()).toBeTruthy();
for (var contextName in contextNameToExpectedValue) {
if (!contextNameToExpectedValue.hasOwnProperty(contextName)) {
continue;
}
expect(this.instance().context[contextName])
.toEqual(contextNameToExpectedValue[contextName]);
}
return this;
}
});