Skip to content

Commit

Permalink
Add a feature flag to disable legacy context (#16269)
Browse files Browse the repository at this point in the history
* Add a feature flag to disable legacy context

* Address review

- invariant -> warning
- Make this.context and context argument actually undefined

* Increase test coverage for lifecycles

* Also disable it on the server is flag is on

* Make this.context {} when disabled, but function context is undefined

* Move checks inside
  • Loading branch information
gaearon committed Aug 2, 2019
1 parent 95674af commit 0c1ec04
Show file tree
Hide file tree
Showing 14 changed files with 810 additions and 278 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');

let React;
let ReactDOM;
let ReactFeatureFlags;
let ReactDOMServer;
let ReactTestUtils;

function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
ReactTestUtils = require('react-dom/test-utils');

ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.disableLegacyContext = true;

// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
ReactTestUtils,
};
}

const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);

function formatValue(val) {
if (val === null) {
return 'null';
}
if (val === undefined) {
return 'undefined';
}
if (typeof val === 'string') {
return val;
}
return JSON.stringify(val);
}

describe('ReactDOMServerIntegrationLegacyContextDisabled', () => {
beforeEach(() => {
resetModules();
});

itRenders('undefined legacy context with warning', async render => {
class LegacyProvider extends React.Component {
static childContextTypes = {
foo() {},
};
getChildContext() {
return {foo: 10};
}
render() {
return this.props.children;
}
}

let lifecycleContextLog = [];
class LegacyClsConsumer extends React.Component {
static contextTypes = {
foo() {},
};
shouldComponentUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
return true;
}
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
lifecycleContextLog.push(nextContext);
}
UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
}
render() {
return formatValue(this.context);
}
}

function LegacyFnConsumer(props, context) {
return formatValue(context);
}
LegacyFnConsumer.contextTypes = {foo() {}};

function RegularFn(props, context) {
return formatValue(context);
}

const e = await render(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
3,
);
expect(e.textContent).toBe('{}undefinedundefined');
expect(lifecycleContextLog).toEqual([]);
});

itRenders('modern context', async render => {
let Ctx = React.createContext();

class Provider extends React.Component {
render() {
return (
<Ctx.Provider value={this.props.value}>
{this.props.children}
</Ctx.Provider>
);
}
}

class RenderPropConsumer extends React.Component {
render() {
return <Ctx.Consumer>{value => formatValue(value)}</Ctx.Consumer>;
}
}

let lifecycleContextLog = [];
class ContextTypeConsumer extends React.Component {
static contextType = Ctx;
shouldComponentUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
return true;
}
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
lifecycleContextLog.push(nextContext);
}
UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
}
render() {
return formatValue(this.context);
}
}

function FnConsumer() {
return formatValue(React.useContext(Ctx));
}

const e = await render(
<Provider value="a">
<span>
<RenderPropConsumer />
<ContextTypeConsumer />
<FnConsumer />
</span>
</Provider>,
);
expect(e.textContent).toBe('aaa');
expect(lifecycleContextLog).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

let React;
let ReactDOM;
let ReactFeatureFlags;

describe('ReactLegacyContextDisabled', () => {
beforeEach(() => {
jest.resetModules();

React = require('react');
ReactDOM = require('react-dom');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.disableLegacyContext = true;
});

function formatValue(val) {
if (val === null) {
return 'null';
}
if (val === undefined) {
return 'undefined';
}
if (typeof val === 'string') {
return val;
}
return JSON.stringify(val);
}

it('warns for legacy context', () => {
class LegacyProvider extends React.Component {
static childContextTypes = {
foo() {},
};
getChildContext() {
return {foo: 10};
}
render() {
return this.props.children;
}
}

let lifecycleContextLog = [];
class LegacyClsConsumer extends React.Component {
static contextTypes = {
foo() {},
};
shouldComponentUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
return true;
}
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
lifecycleContextLog.push(nextContext);
}
UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
}
render() {
return formatValue(this.context);
}
}

function LegacyFnConsumer(props, context) {
return formatValue(context);
}
LegacyFnConsumer.contextTypes = {foo() {}};

function RegularFn(props, context) {
return formatValue(context);
}

const container = document.createElement('div');
expect(() => {
ReactDOM.render(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
container,
);
}).toWarnDev(
[
'LegacyProvider uses the legacy childContextTypes API which is no longer supported. ' +
'Use React.createContext() instead.',
'LegacyClsConsumer uses the legacy contextTypes API which is no longer supported. ' +
'Use React.createContext() with static contextType instead.',
'LegacyFnConsumer uses the legacy contextTypes API which is no longer supported. ' +
'Use React.createContext() with React.useContext() instead.',
],
{withoutStack: true},
);
expect(container.textContent).toBe('{}undefinedundefined');
expect(lifecycleContextLog).toEqual([]);

// Test update path.
ReactDOM.render(
<LegacyProvider>
<span>
<LegacyClsConsumer />
<LegacyFnConsumer />
<RegularFn />
</span>
</LegacyProvider>,
container,
);
expect(container.textContent).toBe('{}undefinedundefined');
expect(lifecycleContextLog).toEqual([{}, {}, {}]);
ReactDOM.unmountComponentAtNode(container);
});

it('renders a tree with modern context', () => {
let Ctx = React.createContext();

class Provider extends React.Component {
render() {
return (
<Ctx.Provider value={this.props.value}>
{this.props.children}
</Ctx.Provider>
);
}
}

class RenderPropConsumer extends React.Component {
render() {
return <Ctx.Consumer>{value => formatValue(value)}</Ctx.Consumer>;
}
}

let lifecycleContextLog = [];
class ContextTypeConsumer extends React.Component {
static contextType = Ctx;
shouldComponentUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
return true;
}
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
lifecycleContextLog.push(nextContext);
}
UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
lifecycleContextLog.push(nextContext);
}
render() {
return formatValue(this.context);
}
}

function FnConsumer() {
return formatValue(React.useContext(Ctx));
}

const container = document.createElement('div');
ReactDOM.render(
<Provider value="a">
<span>
<RenderPropConsumer />
<ContextTypeConsumer />
<FnConsumer />
</span>
</Provider>,
container,
);
expect(container.textContent).toBe('aaa');
expect(lifecycleContextLog).toEqual([]);

// Test update path
ReactDOM.render(
<Provider value="a">
<span>
<RenderPropConsumer />
<ContextTypeConsumer />
<FnConsumer />
</span>
</Provider>,
container,
);
expect(container.textContent).toBe('aaa');
expect(lifecycleContextLog).toEqual(['a', 'a', 'a']);
lifecycleContextLog.length = 0;

ReactDOM.render(
<Provider value="b">
<span>
<RenderPropConsumer />
<ContextTypeConsumer />
<FnConsumer />
</span>
</Provider>,
container,
);
expect(container.textContent).toBe('bbb');
expect(lifecycleContextLog).toEqual(['b', 'b']); // sCU skipped due to changed context value.
ReactDOM.unmountComponentAtNode(container);
});
});

0 comments on commit 0c1ec04

Please sign in to comment.