diff --git a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js
index 6e237e6537..844a9e8026 100644
--- a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js
+++ b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js
@@ -70,11 +70,10 @@ describe('ReactContextValidator', () => {
expect(instance.refs.child.context).toEqual({foo: 'abc'});
});
- it('should filter context properly in callbacks', () => {
+ it('should pass next context to lifecycles', () => {
var actualComponentWillReceiveProps;
var actualShouldComponentUpdate;
var actualComponentWillUpdate;
- var actualComponentDidUpdate;
var Parent = React.createClass({
childContextTypes: {
@@ -113,6 +112,45 @@ describe('ReactContextValidator', () => {
actualComponentWillUpdate = nextContext;
},
+ render: function() {
+ return
;
+ },
+ });
+
+ var container = document.createElement('div');
+ ReactDOM.render(, container);
+ ReactDOM.render(, container);
+ expect(actualComponentWillReceiveProps).toEqual({foo: 'def'});
+ expect(actualShouldComponentUpdate).toEqual({foo: 'def'});
+ expect(actualComponentWillUpdate).toEqual({foo: 'def'});
+ });
+
+ it('should pass previous context to lifecycles', () => {
+ var actualComponentDidUpdate;
+
+ var Parent = React.createClass({
+ childContextTypes: {
+ foo: React.PropTypes.string.isRequired,
+ bar: React.PropTypes.string.isRequired,
+ },
+
+ getChildContext: function() {
+ return {
+ foo: this.props.foo,
+ bar: 'bar',
+ };
+ },
+
+ render: function() {
+ return ;
+ },
+ });
+
+ var Component = React.createClass({
+ contextTypes: {
+ foo: React.PropTypes.string,
+ },
+
componentDidUpdate: function(prevProps, prevState, prevContext) {
actualComponentDidUpdate = prevContext;
},
@@ -125,9 +163,6 @@ describe('ReactContextValidator', () => {
var container = document.createElement('div');
ReactDOM.render(, container);
ReactDOM.render(, container);
- expect(actualComponentWillReceiveProps).toEqual({foo: 'def'});
- expect(actualShouldComponentUpdate).toEqual({foo: 'def'});
- expect(actualComponentWillUpdate).toEqual({foo: 'def'});
expect(actualComponentDidUpdate).toEqual({foo: 'abc'});
});
diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js
index b35567478c..55f7128f52 100644
--- a/src/renderers/shared/fiber/ReactFiberBeginWork.js
+++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js
@@ -23,7 +23,13 @@ var {
reconcileChildFibersInPlace,
cloneChildFibers,
} = require('ReactChildFiber');
+
var ReactTypeOfWork = require('ReactTypeOfWork');
+var {
+ getMaskedContext,
+ pushContextProvider,
+ resetContext,
+} = require('ReactFiberContext');
var {
IndeterminateComponent,
FunctionalComponent,
@@ -144,6 +150,7 @@ module.exports = function(
function updateFunctionalComponent(current, workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
+ var context = getMaskedContext(workInProgress);
// TODO: Disable this before release, since it is not part of the public API
// I use this for testing to compare the relative overhead of classes.
@@ -159,9 +166,9 @@ module.exports = function(
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
- nextChildren = fn(props);
+ nextChildren = fn(props, context);
} else {
- nextChildren = fn(props);
+ nextChildren = fn(props, context);
}
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
@@ -182,11 +189,14 @@ module.exports = function(
} else {
shouldUpdate = updateClassInstance(current, workInProgress);
}
+ const instance = workInProgress.stateNode;
+ if (typeof instance.getChildContext === 'function') {
+ pushContextProvider(workInProgress);
+ }
if (!shouldUpdate) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
// Rerender
- const instance = workInProgress.stateNode;
ReactCurrentOwner.current = workInProgress;
const nextChildren = instance.render();
reconcileChildren(current, workInProgress, nextChildren);
@@ -249,13 +259,15 @@ module.exports = function(
}
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
+ var context = getMaskedContext(workInProgress);
+
var value;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
- value = fn(props);
+ value = fn(props, context);
} else {
- value = fn(props);
+ value = fn(props, context);
}
if (typeof value === 'object' && value && typeof value.render === 'function') {
@@ -355,6 +367,9 @@ module.exports = function(
}
function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
+ if (!workInProgress.return) {
+ resetContext();
+ }
if (workInProgress.pendingWorkPriority === NoWork ||
workInProgress.pendingWorkPriority > priorityLevel) {
return bailoutOnLowPriority(current, workInProgress);
diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js
index fb8a0352fa..aa600db5c3 100644
--- a/src/renderers/shared/fiber/ReactFiberClassComponent.js
+++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js
@@ -15,19 +15,21 @@
import type { Fiber } from 'ReactFiber';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
+var {
+ getMaskedContext,
+} = require('ReactFiberContext');
var {
createUpdateQueue,
addToQueue,
addCallbackToQueue,
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
-var { isMounted } = require('ReactFiberTreeReflection');
+var { getComponentName, isMounted } = require('ReactFiberTreeReflection');
var ReactInstanceMap = require('ReactInstanceMap');
var shallowEqual = require('shallowEqual');
var warning = require('warning');
var invariant = require('invariant');
-
const isArray = Array.isArray;
module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
@@ -74,7 +76,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
},
};
- function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState) {
+ function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) {
const updateQueue = workInProgress.updateQueue;
if (oldProps === null || (updateQueue && updateQueue.isForced)) {
return true;
@@ -82,14 +84,14 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
- const shouldUpdate = instance.shouldComponentUpdate(newProps, newState);
+ const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext);
if (__DEV__) {
warning(
shouldUpdate !== undefined,
'%s.shouldComponentUpdate(): Returned undefined instead of a ' +
'boolean value. Make sure to return true or false.',
- getName(workInProgress, instance)
+ getComponentName(workInProgress)
);
}
@@ -107,19 +109,9 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
return true;
}
- function getName(workInProgress: Fiber, inst: any): string {
- const type = workInProgress.type;
- const constructor = inst && inst.constructor;
- return (
- type.displayName || (constructor && constructor.displayName) ||
- type.name || (constructor && constructor.name) ||
- 'A Component'
- );
- }
-
function checkClassInstance(workInProgress: Fiber, inst: any) {
if (__DEV__) {
- const name = getName(workInProgress, inst);
+ const name = getComponentName(workInProgress);
const renderPresent = inst.render;
warning(
renderPresent,
@@ -194,7 +186,15 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
invariant(
false,
'%s.state: must be set to an object or null',
- getName(workInProgress, inst)
+ getComponentName(workInProgress)
+ );
+ }
+ if (typeof inst.getChildContext === 'function') {
+ invariant(
+ typeof workInProgress.type.childContextTypes === 'object',
+ '%s.getChildContext(): childContextTypes must be defined in order to ' +
+ 'use getChildContext().',
+ getComponentName(workInProgress)
);
}
}
@@ -209,7 +209,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
function constructClassInstance(workInProgress : Fiber) : any {
const ctor = workInProgress.type;
const props = workInProgress.pendingProps;
- const instance = new ctor(props);
+ const context = getMaskedContext(workInProgress);
+ const instance = new ctor(props, context);
checkClassInstance(workInProgress, instance);
adoptClassInstance(workInProgress, instance);
return instance;
@@ -218,7 +219,6 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(workInProgress : Fiber) : void {
const instance = workInProgress.stateNode;
-
const state = instance.state || null;
let props = workInProgress.pendingProps;
@@ -228,6 +228,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
instance.props = props;
instance.state = state;
+ instance.context = getMaskedContext(workInProgress);
if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
@@ -253,6 +254,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
throw new Error('There should always be pending or memoized props.');
}
}
+ const newContext = getMaskedContext(workInProgress);
// TODO: Should we deal with a setState that happened after the last
// componentWillMount and before this componentWillMount? Probably
@@ -262,7 +264,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
workInProgress,
workInProgress.memoizedProps,
newProps,
- newState
+ newState,
+ newContext
)) {
return false;
}
@@ -272,6 +275,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
const newInstance = constructClassInstance(workInProgress);
newInstance.props = newProps;
newInstance.state = newState = newInstance.state || null;
+ newInstance.context = getMaskedContext(workInProgress);
if (typeof newInstance.componentWillMount === 'function') {
newInstance.componentWillMount();
@@ -300,14 +304,16 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
throw new Error('There should always be pending or memoized props.');
}
}
+ const oldContext = instance.context;
+ const newContext = getMaskedContext(workInProgress);
// Note: During these life-cycles, instance.props/instance.state are what
// ever the previously attempted to render - not the "current". However,
// during componentDidUpdate we pass the "current" props.
- if (oldProps !== newProps) {
+ if (oldProps !== newProps || oldContext !== newContext) {
if (typeof instance.componentWillReceiveProps === 'function') {
- instance.componentWillReceiveProps(newProps);
+ instance.componentWillReceiveProps(newProps, newContext);
}
}
@@ -328,6 +334,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
if (oldProps === newProps &&
previousState === newState &&
+ oldContext === newContext &&
updateQueue && !updateQueue.isForced) {
return false;
}
@@ -336,18 +343,20 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
workInProgress,
oldProps,
newProps,
- newState
+ newState,
+ newContext
)) {
// TODO: Should this get the new props/state updated regardless?
return false;
}
if (typeof instance.componentWillUpdate === 'function') {
- instance.componentWillUpdate(newProps, newState);
+ instance.componentWillUpdate(newProps, newState, newContext);
}
instance.props = newProps;
instance.state = newState;
+ instance.context = newContext;
return true;
}
diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
index b0eea7c87e..1444581805 100644
--- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
@@ -18,6 +18,7 @@ import type { HostConfig } from 'ReactFiberReconciler';
import type { ReifiedYield } from 'ReactReifiedYield';
var { reconcileChildFibers } = require('ReactChildFiber');
+var { popContextProvider } = require('ReactFiberContext');
var ReactTypeOfWork = require('ReactTypeOfWork');
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
var {
@@ -120,10 +121,11 @@ module.exports = function(config : HostConfig) {
function completeWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
switch (workInProgress.tag) {
- case FunctionalComponent:
+ case FunctionalComponent: {
transferOutput(workInProgress.child, workInProgress);
return null;
- case ClassComponent:
+ }
+ case ClassComponent: {
transferOutput(workInProgress.child, workInProgress);
// Don't use the state queue to compute the memoized state. We already
// merged it and assigned it to the instance. Transfer it from there.
@@ -148,8 +150,13 @@ module.exports = function(config : HostConfig) {
workInProgress.callbackList = updateQueue;
markCallback(workInProgress);
}
+ const instance = workInProgress.stateNode;
+ if (typeof instance.getChildContext === 'function') {
+ popContextProvider();
+ }
return null;
- case HostContainer:
+ }
+ case HostContainer: {
transferOutput(workInProgress.child, workInProgress);
// We don't know if a container has updated any children so we always
// need to update it right now. We schedule this side-effect before
@@ -158,7 +165,8 @@ module.exports = function(config : HostConfig) {
// are invoked.
markUpdate(workInProgress);
return null;
- case HostComponent:
+ }
+ case HostComponent: {
let newProps = workInProgress.pendingProps;
if (current && workInProgress.stateNode != null) {
// If we have an alternate, that means this is an update and we need to
@@ -200,7 +208,8 @@ module.exports = function(config : HostConfig) {
}
workInProgress.memoizedProps = newProps;
return null;
- case HostText:
+ }
+ case HostText: {
let newText = workInProgress.pendingProps;
if (current && workInProgress.stateNode != null) {
// If we have an alternate, that means this is an update and we need to
@@ -222,25 +231,32 @@ module.exports = function(config : HostConfig) {
}
workInProgress.memoizedProps = newText;
return null;
- case CoroutineComponent:
+ }
+ case CoroutineComponent: {
return moveCoroutineToHandlerPhase(current, workInProgress);
- case CoroutineHandlerPhase:
+ }
+ case CoroutineHandlerPhase: {
transferOutput(workInProgress.stateNode, workInProgress);
// Reset the tag to now be a first phase coroutine.
workInProgress.tag = CoroutineComponent;
return null;
- case YieldComponent:
+ }
+ case YieldComponent: {
// Does nothing.
return null;
- case Fragment:
+ }
+ case Fragment: {
transferOutput(workInProgress.child, workInProgress);
return null;
+ }
// Error cases
- case IndeterminateComponent:
+ case IndeterminateComponent: {
throw new Error('An indeterminate component should have become determinate before completing.');
- default:
+ }
+ default: {
throw new Error('Unknown unit of work tag');
+ }
}
}
diff --git a/src/renderers/shared/fiber/ReactFiberContext.js b/src/renderers/shared/fiber/ReactFiberContext.js
new file mode 100644
index 0000000000..4aa9310e2b
--- /dev/null
+++ b/src/renderers/shared/fiber/ReactFiberContext.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactFiberContext
+ * @flow
+ */
+
+'use strict';
+
+import type { Fiber } from 'ReactFiber';
+
+var emptyObject = require('emptyObject');
+var invariant = require('invariant');
+var {
+ getComponentName,
+} = require('ReactFiberTreeReflection');
+
+if (__DEV__) {
+ var checkReactTypeSpec = require('checkReactTypeSpec');
+}
+
+let index = -1;
+const stack = [];
+
+function getUnmaskedContext() {
+ if (index === -1) {
+ return emptyObject;
+ }
+ return stack[index];
+}
+
+exports.getMaskedContext = function(fiber : Fiber) {
+ const type = fiber.type;
+ const contextTypes = type.contextTypes;
+ if (!contextTypes) {
+ return null;
+ }
+
+ const unmaskedContext = getUnmaskedContext();
+ const context = {};
+ for (let key in contextTypes) {
+ context[key] = unmaskedContext[key];
+ }
+
+ if (__DEV__) {
+ const name = getComponentName(fiber);
+ const debugID = 0; // TODO: pass a real ID
+ checkReactTypeSpec(contextTypes, context, 'context', name, null, debugID);
+ }
+
+ return context;
+};
+
+exports.popContextProvider = function() {
+ stack[index] = emptyObject;
+ index--;
+};
+
+exports.pushContextProvider = function(fiber : Fiber) {
+ const instance = fiber.stateNode;
+ const childContextTypes = fiber.type.childContextTypes;
+ const childContext = instance.getChildContext();
+
+ for (let contextKey in childContext) {
+ invariant(
+ contextKey in childContextTypes,
+ '%s.getChildContext(): key "%s" is not defined in childContextTypes.',
+ getComponentName(fiber),
+ contextKey
+ );
+ }
+ if (__DEV__) {
+ const name = getComponentName(fiber);
+ const debugID = 0; // TODO: pass a real ID
+ checkReactTypeSpec(childContextTypes, childContext, 'childContext', name, null, debugID);
+ }
+
+ const mergedContext = Object.assign({}, getUnmaskedContext(), childContext);
+ index++;
+ stack[index] = mergedContext;
+};
+
+exports.resetContext = function() {
+ index = -1;
+};
+
diff --git a/src/renderers/shared/fiber/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js
index d806992e74..73dd021fb0 100644
--- a/src/renderers/shared/fiber/ReactFiberTreeReflection.js
+++ b/src/renderers/shared/fiber/ReactFiberTreeReflection.js
@@ -114,3 +114,14 @@ exports.findCurrentHostFiber = function(component : ReactComponent {
]);
expect(instance.state.n).toEqual(3);
});
+
+ it('merges and masks context', () => {
+ var ops = [];
+
+ class Intl extends React.Component {
+ static childContextTypes = {
+ locale: React.PropTypes.string,
+ };
+ getChildContext() {
+ return {
+ locale: this.props.locale,
+ };
+ }
+ render() {
+ ops.push('Intl ' + JSON.stringify(this.context));
+ return this.props.children;
+ }
+ }
+
+ class Router extends React.Component {
+ static childContextTypes = {
+ route: React.PropTypes.string,
+ };
+ getChildContext() {
+ return {
+ route: this.props.route,
+ };
+ }
+ render() {
+ ops.push('Router ' + JSON.stringify(this.context));
+ return this.props.children;
+ }
+ }
+
+ class ShowLocale extends React.Component {
+ static contextTypes = {
+ locale: React.PropTypes.string,
+ };
+
+ render() {
+ ops.push('ShowLocale ' + JSON.stringify(this.context));
+ return this.context.locale;
+ }
+ }
+
+ class ShowRoute extends React.Component {
+ static contextTypes = {
+ route: React.PropTypes.string,
+ };
+
+ render() {
+ ops.push('ShowRoute ' + JSON.stringify(this.context));
+ return this.context.route;
+ }
+ }
+
+ function ShowBoth(props, context) {
+ ops.push('ShowBoth ' + JSON.stringify(context));
+ return `${context.route} in ${context.locale}`;
+ }
+ ShowBoth.contextTypes = {
+ locale: React.PropTypes.string,
+ route: React.PropTypes.string,
+ };
+
+ class ShowNeither extends React.Component {
+ render() {
+ ops.push('ShowNeither ' + JSON.stringify(this.context));
+ return null;
+ }
+ }
+
+ class Indirection extends React.Component {
+ render() {
+ ops.push('Indirection ' + JSON.stringify(this.context));
+ return [
+ ,
+ ,
+ ,
+
+
+ ,
+ ,
+ ];
+ }
+ }
+
+ ops.length = [];
+ ReactNoop.render(
+
+
+
+
+
+
+ );
+ ReactNoop.flush();
+ expect(ops).toEqual([
+ 'Intl null',
+ 'ShowLocale {"locale":"fr"}',
+ 'ShowBoth {"locale":"fr"}',
+ ]);
+
+ ops.length = [];
+ ReactNoop.render(
+
+
+
+
+
+
+ );
+ ReactNoop.flush();
+ expect(ops).toEqual([
+ 'Intl null',
+ 'ShowLocale {"locale":"de"}',
+ 'ShowBoth {"locale":"de"}',
+ ]);
+
+ ops.length = [];
+ ReactNoop.render(
+
+
+
+
+
+
+ );
+ ReactNoop.flushDeferredPri(15);
+ expect(ops).toEqual([
+ 'Intl null',
+ ]);
+
+ ops.length = [];
+ ReactNoop.render(
+
+
+
+
+
+
+
+ );
+ ReactNoop.flush();
+ expect(ops).toEqual([
+ 'Intl null',
+ 'ShowLocale {"locale":"en"}',
+ 'Router null',
+ 'Indirection null',
+ 'ShowLocale {"locale":"en"}',
+ 'ShowRoute {"route":"/about"}',
+ 'ShowNeither null',
+ 'Intl null',
+ 'ShowBoth {"locale":"ru","route":"/about"}',
+ 'ShowBoth {"locale":"en","route":"/about"}',
+ 'ShowBoth {"locale":"en"}',
+ ]);
+ });
});