Merge pull request #8272 from gaearon/fiber-context

[Fiber] Initial implementation of context
This commit is contained in:
Dan Abramov
2016-11-15 17:14:12 +00:00
committed by GitHub
11 changed files with 739 additions and 93 deletions
+1 -26
View File
@@ -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
@@ -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
+26
View File
@@ -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
@@ -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 <div />;
},
});
var container = document.createElement('div');
ReactDOM.render(<Parent foo="abc" />, container);
ReactDOM.render(<Parent foo="def" />, 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 <Component />;
},
});
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(<Parent foo="abc" />, container);
ReactDOM.render(<Parent foo="def" />, container);
expect(actualComponentWillReceiveProps).toEqual({foo: 'def'});
expect(actualShouldComponentUpdate).toEqual({foo: 'def'});
expect(actualComponentWillUpdate).toEqual({foo: 'def'});
expect(actualComponentDidUpdate).toEqual({foo: 'abc'});
});
@@ -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<T, P, I, TI, C>(
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<T, P, I, TI, C>(
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<T, P, I, TI, C>(
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<T, P, I, TI, C>(
}
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<T, P, I, TI, C>(
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<T, P, I, TI, C>(
}
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<T, P, I, TI, C>(
workInProgress.memoizedProps !== null &&
workInProgress.pendingProps === workInProgress.memoizedProps
)) &&
workInProgress.updateQueue === null) {
workInProgress.updateQueue === null &&
!hasContextChanged()) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
@@ -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;
}
@@ -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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
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
@@ -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<Object> = [];
const didPerformWorkStack : Array<boolean> = [];
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;
};
@@ -114,3 +114,14 @@ exports.findCurrentHostFiber = function(component : ReactComponent<any, any, any
}
return null;
};
exports.getComponentName = function(fiber: Fiber): string {
const type = fiber.type;
const instance = fiber.stateNode;
const constructor = instance && instance.constructor;
return (
type.displayName || (constructor && constructor.displayName) ||
type.name || (constructor && constructor.name) ||
'A Component'
);
};
@@ -1509,4 +1509,441 @@ describe('ReactIncremental', () => {
]);
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 [
<ShowLocale />,
<ShowRoute />,
<ShowNeither />,
<Intl locale="ru">
<ShowBoth />
</Intl>,
<ShowBoth />,
];
}
}
ops.length = 0;
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>
);
ReactNoop.flush();
expect(ops).toEqual([
'Intl {}',
'ShowLocale {"locale":"fr"}',
'ShowBoth {"locale":"fr"}',
]);
ops.length = 0;
ReactNoop.render(
<Intl locale="de">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>
);
ReactNoop.flush();
expect(ops).toEqual([
'Intl {}',
'ShowLocale {"locale":"de"}',
'ShowBoth {"locale":"de"}',
]);
ops.length = 0;
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>
);
ReactNoop.flushDeferredPri(15);
expect(ops).toEqual([
'Intl {}',
]);
ops.length = 0;
ReactNoop.render(
<Intl locale="en">
<ShowLocale />
<Router route="/about">
<Indirection />
</Router>
<ShowBoth />
</Intl>
);
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 <Recurse />;
}
}
ReactNoop.render(<Recurse />);
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(
<Intl locale="fr">
<ShowLocale />
<div hidden="true">
<ShowLocale />
<Intl locale="ru">
<ShowLocale />
</Intl>
</div>
<ShowLocale />
</Intl>
);
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(
<Intl locale="fr">
<IndirectionFn>
<IndirectionClass>
<Stateful>
<ShowLocaleClass />
<ShowLocaleFn />
</Stateful>
</IndirectionClass>
</IndirectionFn>
</Intl>
);
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 (
<Intl locale={this.state.locale}>
{this.props.children}
</Intl>
);
}
}
ops.length = 0;
ReactNoop.render(
<Stateful>
<IndirectionFn>
<IndirectionClass>
<ShowLocaleClass />
<ShowLocaleFn />
</IndirectionClass>
</IndirectionFn>
</Stateful>
);
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"}',
]);
});
});
@@ -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 <div className="parent" onClick={this.onClick}>{this.props.children}</div>;
return <div className="parent">{this.props.children}</div>;
}
}
var div = document.createElement('div');
var parentInstance = null;
ReactDOM.render(
<Parent>
<Parent ref={inst => parentInstance = inst}>
<ChildWithoutContext>
A1
<GrandChild>A2</GrandChild>
@@ -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