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"}', + ]); + }); });