Implement basic support for context

This commit is contained in:
Dan Abramov
2016-11-11 21:14:20 +00:00
parent 9a42f8a0c5
commit 12bee76ff7
7 changed files with 380 additions and 45 deletions

View File

@@ -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'});
});

View File

@@ -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<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 +166,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;
@@ -182,11 +189,14 @@ module.exports = function<T, P, I, TI, C>(
} 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<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') {
@@ -355,6 +367,9 @@ module.exports = function<T, P, I, TI, C>(
}
function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
if (!workInProgress.return) {
resetContext();
}
if (workInProgress.pendingWorkPriority === NoWork ||
workInProgress.pendingWorkPriority > priorityLevel) {
return bailoutOnLowPriority(current, workInProgress);

View File

@@ -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;
}

View File

@@ -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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// 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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
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<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
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');
}
}
}

View File

@@ -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;
};

View File

@@ -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'
);
};

View File

@@ -1509,4 +1509,162 @@ 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 = [];
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>
);
ReactNoop.flush();
expect(ops).toEqual([
'Intl null',
'ShowLocale {"locale":"fr"}',
'ShowBoth {"locale":"fr"}',
]);
ops.length = [];
ReactNoop.render(
<Intl locale="de">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>
);
ReactNoop.flush();
expect(ops).toEqual([
'Intl null',
'ShowLocale {"locale":"de"}',
'ShowBoth {"locale":"de"}',
]);
ops.length = [];
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>
);
ReactNoop.flushDeferredPri(15);
expect(ops).toEqual([
'Intl null',
]);
ops.length = [];
ReactNoop.render(
<Intl locale="en">
<ShowLocale />
<Router route="/about">
<Indirection />
</Router>
<ShowBoth />
</Intl>
);
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"}',
]);
});
});