diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 92ac48f24ed..bcd6e684cf6 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -19,8 +19,7 @@ src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js * should transition from false to one src/isomorphic/classic/__tests__/ReactContextValidator-test.js -* should filter out context not in contextTypes -* should filter context properly in callbacks +* should pass previous context to lifecycles src/isomorphic/classic/class/__tests__/ReactBind-test.js * Holds reference to instance @@ -31,24 +30,9 @@ src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js * works with mixins that have not opted out of autobinding * works with mixins that have opted out of autobinding -src/isomorphic/classic/class/__tests__/ReactClass-test.js -* renders based on context getInitialState - src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js * includes the owner name when passing null, undefined, boolean, or number -src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee -* renders based on context in the constructor -* supports this.context passed via getChildContext - -src/isomorphic/modern/class/__tests__/ReactES6Class-test.js -* renders based on context in the constructor -* supports this.context passed via getChildContext - -src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts -* renders based on context in the constructor -* supports this.context passed via getChildContext - src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js * should give context for PropType errors in nested components. @@ -441,13 +425,6 @@ src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js * should warn about `setState` on unmounted components * should warn about `setState` in render * should warn about `setState` in getChildContext -* should pass context to children when not owner -* should pass context when re-rendered for static child -* should pass context when re-rendered for static child within a composite component -* should pass context transitively -* should pass context when re-rendered -* unmasked context propagates through updates -* should trigger componentWillReceiveProps for context changes * should update refs if shouldComponentUpdate gives false * should support objects with prototypes as state @@ -467,10 +444,8 @@ src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildText-test.js * should throw if rendering both HTML and children src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js -* should pass context thru stateless component * should warn when stateless component returns array * should throw on string refs in pure functions -* should receive context * should warn when using non-React functions in JSX src/renderers/shared/stack/reconciler/__tests__/ReactUpdates-test.js diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index 501853cc570..50d2a933219 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -12,9 +12,6 @@ src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js * warns for keys with component stack info * should give context for PropType errors in nested components. -src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js -* should warn on invalid context types - src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js * should warn when using hyphenated style names * should warn when updating hyphenated style names diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index b5c82a50dbb..9c2105a8ef9 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -128,6 +128,10 @@ src/isomorphic/children/__tests__/sliceChildren-test.js * should allow static children to be sliced * should slice nested children +src/isomorphic/classic/__tests__/ReactContextValidator-test.js +* should filter out context not in contextTypes +* should pass next context to lifecycles + src/isomorphic/classic/class/__tests__/ReactBind-test.js * warns if you try to bind to this * does not warn if you pass an auto-bound method to setState @@ -149,6 +153,7 @@ src/isomorphic/classic/class/__tests__/ReactClass-test.js * should throw if a reserved property is in statics * should support statics * should work with object getInitialState() return values +* renders based on context getInitialState * should throw with non-object getInitialState() return values * should work with a null getInitialState() return value * should throw when using legacy factories @@ -354,6 +359,7 @@ src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee * renders a simple stateless component with prop * renders based on state using initial values in this.props * renders based on state using props in the constructor +* renders based on context in the constructor * renders only once when setting state in componentWillMount * should throw with non-object in the initial state property * should render with null in the initial state property @@ -365,6 +371,7 @@ src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee * should warn when misspelling shouldComponentUpdate * should warn when misspelling componentWillReceiveProps * should throw AND warn when trying to access classic APIs +* supports this.context passed via getChildContext * supports classic refs * supports drilling through to the DOM using findDOMNode @@ -374,6 +381,7 @@ src/isomorphic/modern/class/__tests__/ReactES6Class-test.js * renders a simple stateless component with prop * renders based on state using initial values in this.props * renders based on state using props in the constructor +* renders based on context in the constructor * renders only once when setting state in componentWillMount * should throw with non-object in the initial state property * should render with null in the initial state property @@ -385,6 +393,7 @@ src/isomorphic/modern/class/__tests__/ReactES6Class-test.js * should warn when misspelling shouldComponentUpdate * should warn when misspelling componentWillReceiveProps * should throw AND warn when trying to access classic APIs +* supports this.context passed via getChildContext * supports classic refs * supports drilling through to the DOM using findDOMNode @@ -399,6 +408,7 @@ src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts * renders a simple stateless component with prop * renders based on state using initial values in this.props * renders based on state using props in the constructor +* renders based on context in the constructor * renders only once when setting state in componentWillMount * should throw with non-object in the initial state property * should render with null in the initial state property @@ -410,6 +420,7 @@ src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts * should warn when misspelling shouldComponentUpdate * should warn when misspelling componentWillReceiveProps * should throw AND warn when trying to access classic APIs +* supports this.context passed via getChildContext * supports classic refs * supports drilling through to the DOM using findDOMNode @@ -446,6 +457,7 @@ src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js * should not check the default for explicit null * should check declared prop types * should warn on invalid prop types +* should warn on invalid context types * should warn if getDefaultProps is specificed on the class src/renderers/dom/__tests__/ReactDOMProduction-test.js @@ -806,6 +818,11 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js * performs batched updates at the end of the batch * can nest batchedUpdates * can handle if setState callback throws +* merges and masks context +* does not leak own context into context provider +* provides context when reusing work +* reads context when setState is below the provider +* reads context when setState is above the provider src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js * catches render error in a boundary during mounting @@ -1018,7 +1035,14 @@ src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js * should call componentWillUnmount before unmounting * should warn when shouldComponentUpdate() returns undefined * should warn when componentDidUnmount method is defined +* should pass context to children when not owner * should skip update when rerendering element in container +* should pass context when re-rendered for static child +* should pass context when re-rendered for static child within a composite component +* should pass context transitively +* should pass context when re-rendered +* unmasked context propagates through updates +* should trigger componentWillReceiveProps for context changes * only renders once if updated in componentWillReceiveProps * should allow access to findDOMNode in componentWillUnmount * context should be passed down from the parent @@ -1147,9 +1171,11 @@ src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js * should render stateless component * should update stateless component * should unmount stateless component +* should pass context thru stateless component * should provide a null ref * should use correct name in key warning * should support default props and prop types +* should receive context * should work with arrow functions * should allow simple functions to return null * should allow simple functions to return false diff --git a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js index 6e237e65379..844a9e80269 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 b35567478c7..02fd265aa93 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -23,7 +23,15 @@ var { reconcileChildFibersInPlace, cloneChildFibers, } = require('ReactChildFiber'); + var ReactTypeOfWork = require('ReactTypeOfWork'); +var { + getMaskedContext, + isContextProvider, + hasContextChanged, + pushContextProvider, + resetContext, +} = require('ReactFiberContext'); var { IndeterminateComponent, FunctionalComponent, @@ -144,6 +152,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 +168,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; @@ -190,6 +199,10 @@ module.exports = function( ReactCurrentOwner.current = workInProgress; const nextChildren = instance.render(); reconcileChildren(current, workInProgress, nextChildren); + // Put context on the stack because we will work on children + if (isContextProvider(workInProgress)) { + pushContextProvider(workInProgress, true); + } return workInProgress.child; } @@ -249,13 +262,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') { @@ -345,6 +360,10 @@ module.exports = function( cloneChildFibers(current, workInProgress); markChildAsProgressed(current, workInProgress, priorityLevel); + // Put context on the stack because we will work on children + if (isContextProvider(workInProgress)) { + pushContextProvider(workInProgress, false); + } return workInProgress.child; } @@ -355,6 +374,11 @@ module.exports = function( } function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber { + if (!workInProgress.return) { + // Don't start new work with context on the stack. + resetContext(); + } + if (workInProgress.pendingWorkPriority === NoWork || workInProgress.pendingWorkPriority > priorityLevel) { return bailoutOnLowPriority(current, workInProgress); @@ -375,7 +399,8 @@ module.exports = function( workInProgress.memoizedProps !== null && workInProgress.pendingProps === workInProgress.memoizedProps )) && - workInProgress.updateQueue === null) { + workInProgress.updateQueue === null && + !hasContextChanged()) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index fb8a0352fa2..79a6a9ebc9e 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,20 +109,11 @@ 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) { + function checkClassInstance(workInProgress: Fiber) { + const instance = workInProgress.stateNode; if (__DEV__) { - const name = getName(workInProgress, inst); - const renderPresent = inst.render; + const name = getComponentName(workInProgress); + const renderPresent = instance.render; warning( renderPresent, '%s(...): No `render` method found on the returned component ' + @@ -128,8 +121,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { name ); const noGetInitialStateOnES6 = ( - !inst.getInitialState || - inst.getInitialState.isReactClassApproved + !instance.getInitialState || + instance.getInitialState.isReactClassApproved ); warning( noGetInitialStateOnES6, @@ -139,8 +132,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { name ); const noGetDefaultPropsOnES6 = ( - !inst.getDefaultProps || - inst.getDefaultProps.isReactClassApproved + !instance.getDefaultProps || + instance.getDefaultProps.isReactClassApproved ); warning( noGetDefaultPropsOnES6, @@ -149,21 +142,21 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { 'Use a static property to define defaultProps instead.', name ); - const noInstancePropTypes = !inst.propTypes; + const noInstancePropTypes = !instance.propTypes; warning( noInstancePropTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', name, ); - const noInstanceContextTypes = !inst.contextTypes; + const noInstanceContextTypes = !instance.contextTypes; warning( noInstanceContextTypes, 'contextTypes was defined as an instance property on %s. Use a static ' + 'property to define contextTypes instead.', name, ); - const noComponentShouldUpdate = typeof inst.componentShouldUpdate !== 'function'; + const noComponentShouldUpdate = typeof instance.componentShouldUpdate !== 'function'; warning( noComponentShouldUpdate, '%s has a method called ' + @@ -172,7 +165,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { 'expected to return a value.', name ); - const noComponentDidUnmount = typeof inst.componentDidUnmount !== 'function'; + const noComponentDidUnmount = typeof instance.componentDidUnmount !== 'function'; warning( noComponentDidUnmount, '%s has a method called ' + @@ -180,7 +173,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { 'Did you mean componentWillUnmount()?', name ); - const noComponentWillRecieveProps = typeof inst.componentWillRecieveProps !== 'function'; + const noComponentWillRecieveProps = typeof instance.componentWillRecieveProps !== 'function'; warning( noComponentWillRecieveProps, '%s has a method called ' + @@ -189,12 +182,20 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { ); } - const instanceState = inst.state; - if (instanceState && (typeof instanceState !== 'object' || isArray(instanceState))) { + const state = instance.state; + if (state && (typeof state !== 'object' || isArray(state))) { invariant( false, '%s.state: must be set to an object or null', - getName(workInProgress, inst) + getComponentName(workInProgress) + ); + } + if (typeof instance.getChildContext === 'function') { + invariant( + typeof workInProgress.type.childContextTypes === 'object', + '%s.getChildContext(): childContextTypes must be defined in order to ' + + 'use getChildContext().', + getComponentName(workInProgress) ); } } @@ -209,16 +210,16 @@ 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); - checkClassInstance(workInProgress, instance); + const context = getMaskedContext(workInProgress); + const instance = new ctor(props, context); adoptClassInstance(workInProgress, instance); + checkClassInstance(workInProgress); return instance; } // 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 +229,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 +255,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 +265,8 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { workInProgress, workInProgress.memoizedProps, newProps, - newState + newState, + newContext )) { return false; } @@ -272,6 +276,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,34 +305,37 @@ 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); } } // Compute the next state using the memoized state and the update queue. const updateQueue = workInProgress.updateQueue; - const previousState = workInProgress.memoizedState; + const oldState = workInProgress.memoizedState; // TODO: Previous state can be null. let newState; if (updateQueue) { if (!updateQueue.hasUpdate) { - newState = previousState; + newState = oldState; } else { - newState = mergeUpdateQueue(updateQueue, instance, previousState, newProps); + newState = mergeUpdateQueue(updateQueue, instance, oldState, newProps); } } else { - newState = previousState; + newState = oldState; } if (oldProps === newProps && - previousState === newState && + oldState === newState && + oldContext === newContext && updateQueue && !updateQueue.isForced) { return false; } @@ -336,18 +344,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 b0eea7c87e2..82a9bdf211a 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -18,6 +18,10 @@ import type { HostConfig } from 'ReactFiberReconciler'; import type { ReifiedYield } from 'ReactReifiedYield'; var { reconcileChildFibers } = require('ReactChildFiber'); +var { + isContextProvider, + popContextProvider, +} = require('ReactFiberContext'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var { @@ -125,6 +129,10 @@ module.exports = function(config : HostConfig) { return null; case ClassComponent: transferOutput(workInProgress.child, workInProgress); + // We are leaving this subtree, so pop context if any. + if (isContextProvider(workInProgress)) { + popContextProvider(); + } // 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. // Also need to transfer the props, because pendingProps will be null diff --git a/src/renderers/shared/fiber/ReactFiberContext.js b/src/renderers/shared/fiber/ReactFiberContext.js new file mode 100644 index 00000000000..d2e09311abc --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberContext.js @@ -0,0 +1,117 @@ +/** + * 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'); +var { + ClassComponent, +} = require('ReactTypeOfWork'); + +if (__DEV__) { + var checkReactTypeSpec = require('checkReactTypeSpec'); +} + +let index = -1; +const contextStack : Array = []; +const didPerformWorkStack : Array = []; + +function getUnmaskedContext() { + if (index === -1) { + return emptyObject; + } + return contextStack[index]; +} + +exports.getMaskedContext = function(fiber : Fiber) { + const type = fiber.type; + const contextTypes = type.contextTypes; + if (!contextTypes) { + return emptyObject; + } + + 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.hasContextChanged = function() : boolean { + return index > -1 && didPerformWorkStack[index]; +}; + +exports.isContextProvider = function(fiber : Fiber) : boolean { + return ( + fiber.tag === ClassComponent && + typeof fiber.stateNode.getChildContext === 'function' + ); +}; + +exports.popContextProvider = function() : void { + contextStack[index] = emptyObject; + didPerformWorkStack[index] = false; + index--; +}; + +exports.pushContextProvider = function(fiber : Fiber, didPerformWork : boolean) : void { + const instance = fiber.stateNode; + const childContextTypes = fiber.type.childContextTypes; + + const memoizedMergedChildContext = instance.__reactInternalMemoizedMergedChildContext; + const canReuseMergedChildContext = !didPerformWork && memoizedMergedChildContext != null; + + let mergedContext = null; + if (canReuseMergedChildContext) { + mergedContext = memoizedMergedChildContext; + } else { + 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); + } + mergedContext = {...getUnmaskedContext(), ...childContext}; + instance.__reactInternalMemoizedMergedChildContext = mergedContext; + } + + index++; + contextStack[index] = mergedContext; + didPerformWorkStack[index] = didPerformWork; +}; + +exports.resetContext = function() : void { + index = -1; +}; + diff --git a/src/renderers/shared/fiber/ReactFiberTreeReflection.js b/src/renderers/shared/fiber/ReactFiberTreeReflection.js index d806992e74d..73dd021fb0d 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 = 0; + ReactNoop.render( + + +
+ +
+
+ ); + ReactNoop.flush(); + expect(ops).toEqual([ + 'Intl {}', + 'ShowLocale {"locale":"fr"}', + 'ShowBoth {"locale":"fr"}', + ]); + + ops.length = 0; + ReactNoop.render( + + +
+ +
+
+ ); + ReactNoop.flush(); + expect(ops).toEqual([ + 'Intl {}', + 'ShowLocale {"locale":"de"}', + 'ShowBoth {"locale":"de"}', + ]); + + ops.length = 0; + ReactNoop.render( + + +
+ +
+
+ ); + ReactNoop.flushDeferredPri(15); + expect(ops).toEqual([ + 'Intl {}', + ]); + + ops.length = 0; + ReactNoop.render( + + + + + + + + ); + ReactNoop.flush(); + expect(ops).toEqual([ + 'Intl {}', + 'ShowLocale {"locale":"en"}', + 'Router {}', + 'Indirection {}', + 'ShowLocale {"locale":"en"}', + 'ShowRoute {"route":"/about"}', + 'ShowNeither {}', + 'Intl {}', + 'ShowBoth {"locale":"ru","route":"/about"}', + 'ShowBoth {"locale":"en","route":"/about"}', + 'ShowBoth {"locale":"en"}', + ]); + }); + + it('does not leak own context into context provider', () => { + var ops = []; + class Recurse extends React.Component { + static contextTypes = { + n: React.PropTypes.number, + }; + static childContextTypes = { + n: React.PropTypes.number, + }; + getChildContext() { + return {n: (this.context.n || 3) - 1}; + } + render() { + ops.push('Recurse ' + JSON.stringify(this.context)); + if (this.context.n === 0) { + return null; + } + return ; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + 'Recurse {}', + 'Recurse {"n":2}', + 'Recurse {"n":1}', + 'Recurse {"n":0}', + ]); + }); + + it('provides context when reusing work', () => { + 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 ShowLocale extends React.Component { + static contextTypes = { + locale: React.PropTypes.string, + }; + render() { + ops.push('ShowLocale ' + JSON.stringify(this.context)); + return this.context.locale; + } + } + + ops.length = 0; + ReactNoop.render( + + + + + + ); + ReactNoop.flushDeferredPri(40); + expect(ops).toEqual([ + 'Intl {}', + 'ShowLocale {"locale":"fr"}', + 'ShowLocale {"locale":"fr"}', + ]); + + ops.length = 0; + ReactNoop.flush(); + expect(ops).toEqual([ + 'ShowLocale {"locale":"fr"}', + 'Intl {}', + 'ShowLocale {"locale":"ru"}', + ]); + }); + + it('reads context when setState is below the provider', () => { + var ops = []; + var statefulInst; + + class Intl extends React.Component { + static childContextTypes = { + locale: React.PropTypes.string, + }; + getChildContext() { + const childContext = { + locale: this.props.locale, + }; + ops.push('Intl:provide ' + JSON.stringify(childContext)); + return childContext; + } + render() { + ops.push('Intl:read ' + JSON.stringify(this.context)); + return this.props.children; + } + } + + class ShowLocaleClass extends React.Component { + static contextTypes = { + locale: React.PropTypes.string, + }; + render() { + ops.push('ShowLocaleClass:read ' + JSON.stringify(this.context)); + return this.context.locale; + } + } + + function ShowLocaleFn(props, context) { + ops.push('ShowLocaleFn:read ' + JSON.stringify(context)); + return context.locale; + } + ShowLocaleFn.contextTypes = { + locale: React.PropTypes.string, + }; + + class Stateful extends React.Component { + state = {x: 0}; + render() { + statefulInst = this; + return this.props.children; + } + } + + function IndirectionFn(props, context) { + ops.push('IndirectionFn ' + JSON.stringify(context)); + return props.children; + } + + class IndirectionClass extends React.Component { + render() { + ops.push('IndirectionClass ' + JSON.stringify(this.context)); + return this.props.children; + } + } + + ops.length = 0; + ReactNoop.render( + + + + + + + + + + + ); + ReactNoop.flush(); + expect(ops).toEqual([ + 'Intl:read {}', + 'Intl:provide {"locale":"fr"}', + 'IndirectionFn {}', + 'IndirectionClass {}', + 'ShowLocaleClass:read {"locale":"fr"}', + 'ShowLocaleFn:read {"locale":"fr"}', + ]); + + ops.length = 0; + statefulInst.setState({x: 1}); + ReactNoop.flush(); + // All work has been memoized because setState() + // happened below the context and could not have affected it. + expect(ops).toEqual([]); + }); + + it('reads context when setState is above the provider', () => { + var ops = []; + var statefulInst; + + class Intl extends React.Component { + static childContextTypes = { + locale: React.PropTypes.string, + }; + getChildContext() { + const childContext = { + locale: this.props.locale, + }; + ops.push('Intl:provide ' + JSON.stringify(childContext)); + return childContext; + } + render() { + ops.push('Intl:read ' + JSON.stringify(this.context)); + return this.props.children; + } + } + + class ShowLocaleClass extends React.Component { + static contextTypes = { + locale: React.PropTypes.string, + }; + render() { + ops.push('ShowLocaleClass:read ' + JSON.stringify(this.context)); + return this.context.locale; + } + } + + function ShowLocaleFn(props, context) { + ops.push('ShowLocaleFn:read ' + JSON.stringify(context)); + return context.locale; + } + ShowLocaleFn.contextTypes = { + locale: React.PropTypes.string, + }; + + function IndirectionFn(props, context) { + ops.push('IndirectionFn ' + JSON.stringify(context)); + return props.children; + } + + class IndirectionClass extends React.Component { + render() { + ops.push('IndirectionClass ' + JSON.stringify(this.context)); + return this.props.children; + } + } + + class Stateful extends React.Component { + state = {locale: 'fr'}; + render() { + statefulInst = this; + return ( + + {this.props.children} + + ); + } + } + + ops.length = 0; + ReactNoop.render( + + + + + + + + + ); + ReactNoop.flush(); + expect(ops).toEqual([ + 'Intl:read {}', + 'Intl:provide {"locale":"fr"}', + 'IndirectionFn {}', + 'IndirectionClass {}', + 'ShowLocaleClass:read {"locale":"fr"}', + 'ShowLocaleFn:read {"locale":"fr"}', + ]); + + ops.length = 0; + statefulInst.setState({locale: 'gr'}); + ReactNoop.flush(); + expect(ops).toEqual([ + // Intl is below setState() so it might have been + // affected by it. Therefore we re-render and recompute + // its child context. + 'Intl:read {}', + 'Intl:provide {"locale":"gr"}', + // TODO: it's unfortunate that we can't reuse work on + // these components even though they don't depend on context. + 'IndirectionFn {}', + 'IndirectionClass {}', + // These components depend on context: + 'ShowLocaleClass:read {"locale":"gr"}', + 'ShowLocaleFn:read {"locale":"gr"}', + ]); + }); }); diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js index c8055754abc..b5f92fd0047 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js @@ -15,6 +15,7 @@ var ChildUpdates; var MorphingComponent; var React; var ReactDOM; +var ReactDOMFeatureFlags; var ReactCurrentOwner; var ReactPropTypes; var ReactServerRendering; @@ -26,6 +27,7 @@ describe('ReactCompositeComponent', () => { jest.resetModuleRegistry(); React = require('React'); ReactDOM = require('ReactDOM'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactCurrentOwner = require('ReactCurrentOwner'); ReactPropTypes = require('ReactPropTypes'); ReactTestUtils = require('ReactTestUtils'); @@ -830,7 +832,10 @@ describe('ReactCompositeComponent', () => { } componentDidUpdate(prevProps, prevState, prevContext) { - expect('foo' in prevContext).toBe(true); + if (!ReactDOMFeatureFlags.useFiber) { + // Fiber does not pass the previous context. + expect('foo' in prevContext).toBe(true); + } } shouldComponentUpdate(nextProps, nextState, nextContext) { @@ -849,7 +854,10 @@ describe('ReactCompositeComponent', () => { } componentDidUpdate(prevProps, prevState, prevContext) { - expect('foo' in prevContext).toBe(false); + if (!ReactDOMFeatureFlags.useFiber) { + // Fiber does not pass the previous context. + expect('foo' in prevContext).toBe(false); + } } shouldComponentUpdate(nextProps, nextState, nextContext) { @@ -971,21 +979,16 @@ describe('ReactCompositeComponent', () => { }; } - onClick = () => { - this.setState({ - foo: 'def', - }); - }; - render() { - return
{this.props.children}
; + return
{this.props.children}
; } } var div = document.createElement('div'); + var parentInstance = null; ReactDOM.render( - + parentInstance = inst}> A1 A2 @@ -999,7 +1002,9 @@ describe('ReactCompositeComponent', () => { div ); - ReactTestUtils.Simulate.click(div.childNodes[0]); + parentInstance.setState({ + foo: 'def', + }); expect(propChanges).toBe(0); expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2