diff --git a/.gitignore b/.gitignore index f4dcfd6a95a9d..fd8f479b2654b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ examples/shared/*.js test/the-files-to-test.generated.js *.log* chrome-user-data +.idea \ No newline at end of file diff --git a/examples/context/index.html b/examples/context/index.html new file mode 100644 index 0000000000000..19d52528f9d78 --- /dev/null +++ b/examples/context/index.html @@ -0,0 +1,118 @@ + + + + + Context Example + + + +

Basic Example

+
+

+ To install React, follow the instructions on + GitHub. +

+

+ If you can see this, React is not working right. + If you checked out the source from GitHub make sure to run grunt. +

+
+

Example Details

+

This is written in vanilla JavaScript (without JSX) and transformed in the browser.

+

+ Learn more about React at + facebook.github.io/react. +

+ + + + + + + diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js index 11806131b82aa..6a4a295f1eee0 100644 --- a/src/renderers/shared/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js @@ -108,6 +108,12 @@ var ReactCompositeComponentMixin = { // See ReactUpdates and ReactUpdateQueue. this._pendingCallbacks = null; + + // See ReactContext + this._contextChildren = null; // or array + this._contextParent = null; + this._isContextParent = false; + this._isContextChild = false; }, /** @@ -131,6 +137,11 @@ var ReactCompositeComponentMixin = { this._currentElement ); + // This component can be classed as a "context parent" if it modified + // it's child context + this._isContextParent = !!Component.childContextTypes; + this._isContextChild = !!Component.contextTypes || !!Component.childContextTypes; + // Initialize the public class var inst = new Component(publicProps, publicContext); @@ -158,6 +169,19 @@ var ReactCompositeComponentMixin = { // Store a reference from the instance back to the internal representation ReactInstanceMap.set(inst, this); + // Setup context parent/child relationship + var previousContextParent = ReactContext.currentParent; + if (this._isContextChild) { + // set two way ref + ReactContext.parentChild(this, ReactContext.currentParent); + if (this._isContextParent) { + ReactContext.currentParent = this; + } + } else { + // set one way ref + this._isContextParent = ReactContext.currentParent; + } + if (__DEV__) { // Since plain JS classes are defined without any special initialization // logic, we can not catch common errors early. Therefore, we have to @@ -238,12 +262,16 @@ var ReactCompositeComponentMixin = { this._currentElement.type // The wrapping type ); - var markup = ReactReconciler.mountComponent( - this._renderedComponent, - rootID, - transaction, - this._processChildContext(context) - ); + try { + var markup = ReactReconciler.mountComponent( + this._renderedComponent, + rootID, + transaction, + this._processChildContext(context) + ); + } finally { + ReactContext.currentParent = previousContextParent; + } if (inst.componentDidMount) { transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); } @@ -290,6 +318,9 @@ var ReactCompositeComponentMixin = { // leaks a reference to the public instance. ReactInstanceMap.remove(inst); + // Dispose of any context parent/child relationship + ReactContext.orphanChild(this); + // Some existing components rely on inst.props even after they've been // destroyed (in event handlers). // TODO: inst.props = null; @@ -497,6 +528,10 @@ var ReactCompositeComponentMixin = { ); }, + receiveContext: function(transaction, nextContext) { + this.receiveComponent(this._currentElement, transaction, nextContext); + }, + /** * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` * is set, update the component. @@ -558,9 +593,19 @@ var ReactCompositeComponentMixin = { var nextContext = inst.context; var nextProps = inst.props; + // Update context parent/child relationship + var previousContextParent = ReactContext.currentParent; + if (this._isContextParent) { + ReactContext.currentParent = this; + } else { + ReactContext.currentParent = this._contextParent; + } + + // TODO: Maybe use some _receivingContext flag? + nextContext = this._processContext(nextUnmaskedContext); + // Distinguish between a props update versus a simple state update if (prevParentElement !== nextParentElement) { - nextContext = this._processContext(nextUnmaskedContext); nextProps = this._processProps(nextParentElement.props); // An update here will schedule an update but immediately set @@ -591,14 +636,18 @@ var ReactCompositeComponentMixin = { if (shouldUpdate) { this._pendingForceUpdate = false; // Will set `this.props`, `this.state` and `this.context`. - this._performComponentUpdate( - nextParentElement, - nextProps, - nextState, - nextContext, - transaction, - nextUnmaskedContext - ); + try { + this._performComponentUpdate( + nextParentElement, + nextProps, + nextState, + nextContext, + transaction, + nextUnmaskedContext + ); + } finally { + ReactContext.currentParent = previousContextParent; + } } else { // If it's determined that a component should not update, we still want // to set props and state but we shortcut the rest of the update. @@ -607,6 +656,41 @@ var ReactCompositeComponentMixin = { inst.props = nextProps; inst.state = nextState; inst.context = nextContext; + + // We will also, potentially, want to update components down the tree + // if the context has updated + var shouldUpdateChildContext = !inst.shouldUpdateChildContext || + inst.shouldUpdateChildContext(nextProps, nextState, nextContext); + + if (shouldUpdateChildContext) { + this._performContextUpdate( + nextUnmaskedContext, + transaction + ) + } + } + }, + + /** + * Notifies delegate methods of update and performs update. + * + * @param {?object} nextUnmaskedContext unmasked context + * @param {ReactReconcileTransaction} transaction + * @private + */ + _performContextUpdate: function( + nextUnmaskedContext, + transaction + ) { + var childContext = this._processChildContext(nextUnmaskedContext); + this._updateContextChildren(transaction, childContext); + }, + + _updateContextChildren: function(transaction, childContext) { + if (this._contextChildren) { + this._contextChildren.forEach(function(child) { + child.receiveContext(transaction, childContext); + }); } }, diff --git a/src/renderers/shared/reconciler/ReactContext.js b/src/renderers/shared/reconciler/ReactContext.js index 75cc7c4d0d438..5be3f3d5f3641 100644 --- a/src/renderers/shared/reconciler/ReactContext.js +++ b/src/renderers/shared/reconciler/ReactContext.js @@ -11,8 +11,33 @@ 'use strict'; +var ReactNativeComponent = require('ReactNativeComponent'); + var emptyObject = require('emptyObject'); +/** + * Array comparator for ReactComponents by mount ordering. + * + * @param {ReactComponent} c1 first component you're comparing + * @param {ReactComponent} c2 second component you're comparing + * @return {number} Return value usable by Array.prototype.sort(). + */ +function mountOrderComparator(c1, c2) { + return c1._mountOrder - c2._mountOrder; +} + +function containsMatchingKey(obj1, obj2) { + var keys = Object.keys(obj1); + + for (var i = 0, l = keys.length; i < l; ++i) { + if (obj2.hasOwnProperty(keys[i])) { + return true; + } + } + + return false; +} + /** * Keeps track of the current context. * @@ -25,7 +50,68 @@ var ReactContext = { * @internal * @type {object} */ - current: emptyObject + current: emptyObject, + + currentParent: null, + + parentChild: function(child, parent) { + if (parent) { + var ParentComponent = ReactNativeComponent.getComponentClassForElement( + parent._currentElement + ); + + var ChildComponent = ReactNativeComponent.getComponentClassForElement( + child._currentElement + ); + + if ( + ChildComponent.contextTypes && + ParentComponent.childContextTypes && + containsMatchingKey( + ChildComponent.contextTypes, + ParentComponent.childContextTypes + ) + ) { + this._addChildToParent(parent, child); + child._contextParent = parent; + + return; + } + + return this.parentChild(child, parent._contextParent); + } + + child._contextParent = null; + }, + + orphanChild: function(child) { + if (!child._contextParent) { + return; + } + + this._removeChildFromParent(child._contextParent, child); + child._contextParent = null; + }, + + _addChildToParent: function(parent, child) { + var children = parent._contextChildren = parent._contextChildren || []; + + children.push(child); + children.sort(mountOrderComparator); + }, + + _removeChildFromParent: function(parent, child) { + var children = parent._contextChildren; + + if (!children) { + return; + } + + var index = children.indexOf(child); + if (index >= 0) { + children.splice(index, 1); + } + } };