Skip to content

Commit 7ad7386

Browse files
authored
Improve warning for invalid class contextType (#15142)
* Improve warning for invalid class contextType * Don't warn for null * Grammar
1 parent 1e3364e commit 7ad7386

File tree

4 files changed

+243
-33
lines changed

4 files changed

+243
-33
lines changed

packages/react-dom/src/__tests__/ReactServerRendering-test.js

+82
Original file line numberDiff line numberDiff line change
@@ -943,4 +943,86 @@ describe('ReactDOMServer', () => {
943943
{withoutStack: true},
944944
);
945945
});
946+
947+
it('should not warn when class contextType is null', () => {
948+
class Foo extends React.Component {
949+
static contextType = null; // Handy for conditional declaration
950+
render() {
951+
return this.context.hello.world;
952+
}
953+
}
954+
955+
expect(() => {
956+
ReactDOMServer.renderToString(<Foo />);
957+
}).toThrow("Cannot read property 'world' of undefined");
958+
});
959+
960+
it('should warn when class contextType is undefined', () => {
961+
class Foo extends React.Component {
962+
// This commonly happens with circular deps
963+
// https://github.com/facebook/react/issues/13969
964+
static contextType = undefined;
965+
render() {
966+
return this.context.hello.world;
967+
}
968+
}
969+
970+
expect(() => {
971+
expect(() => {
972+
ReactDOMServer.renderToString(<Foo />);
973+
}).toThrow("Cannot read property 'world' of undefined");
974+
}).toWarnDev(
975+
'Foo defines an invalid contextType. ' +
976+
'contextType should point to the Context object returned by React.createContext(). ' +
977+
'However, it is set to undefined. ' +
978+
'This can be caused by a typo or by mixing up named and default imports. ' +
979+
'This can also happen due to a circular dependency, ' +
980+
'so try moving the createContext() call to a separate file.',
981+
{withoutStack: true},
982+
);
983+
});
984+
985+
it('should warn when class contextType is an object', () => {
986+
class Foo extends React.Component {
987+
// Can happen due to a typo
988+
static contextType = {
989+
x: 42,
990+
y: 'hello',
991+
};
992+
render() {
993+
return this.context.hello.world;
994+
}
995+
}
996+
997+
expect(() => {
998+
expect(() => {
999+
ReactDOMServer.renderToString(<Foo />);
1000+
}).toThrow("Cannot read property 'hello' of undefined");
1001+
}).toWarnDev(
1002+
'Foo defines an invalid contextType. ' +
1003+
'contextType should point to the Context object returned by React.createContext(). ' +
1004+
'However, it is set to an object with keys {x, y}.',
1005+
{withoutStack: true},
1006+
);
1007+
});
1008+
1009+
it('should warn when class contextType is a primitive', () => {
1010+
class Foo extends React.Component {
1011+
static contextType = 'foo';
1012+
render() {
1013+
return this.context.hello.world;
1014+
}
1015+
}
1016+
1017+
expect(() => {
1018+
expect(() => {
1019+
ReactDOMServer.renderToString(<Foo />);
1020+
}).toThrow("Cannot read property 'world' of undefined");
1021+
}).toWarnDev(
1022+
'Foo defines an invalid contextType. ' +
1023+
'contextType should point to the Context object returned by React.createContext(). ' +
1024+
'However, it is set to a string.',
1025+
{withoutStack: true},
1026+
);
1027+
});
9461028
});

packages/react-dom/src/server/ReactPartialRendererContext.js

+43-20
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@
1010
import type {ThreadID} from './ReactThreadIDAllocator';
1111
import type {ReactContext} from 'shared/ReactTypes';
1212

13-
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
13+
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
1414
import ReactSharedInternals from 'shared/ReactSharedInternals';
1515
import getComponentName from 'shared/getComponentName';
1616
import warningWithoutStack from 'shared/warningWithoutStack';
1717
import checkPropTypes from 'prop-types/checkPropTypes';
1818

1919
let ReactDebugCurrentFrame;
20+
let didWarnAboutInvalidateContextType;
2021
if (__DEV__) {
2122
ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
23+
didWarnAboutInvalidateContextType = new Set();
2224
}
2325

24-
const didWarnAboutInvalidateContextType = {};
25-
2626
export const emptyObject = {};
2727
if (__DEV__) {
2828
Object.freeze(emptyObject);
@@ -75,26 +75,49 @@ export function processContext(
7575
threadID: ThreadID,
7676
) {
7777
const contextType = type.contextType;
78-
if (typeof contextType === 'object' && contextType !== null) {
79-
if (__DEV__) {
80-
const isContextConsumer =
81-
contextType.$$typeof === REACT_CONTEXT_TYPE &&
82-
contextType._context !== undefined;
83-
if (contextType.$$typeof !== REACT_CONTEXT_TYPE || isContextConsumer) {
84-
let name = getComponentName(type) || 'Component';
85-
if (!didWarnAboutInvalidateContextType[name]) {
86-
didWarnAboutInvalidateContextType[name] = true;
87-
warningWithoutStack(
88-
false,
89-
'%s defines an invalid contextType. ' +
90-
'contextType should point to the Context object returned by React.createContext(). ' +
91-
'Did you accidentally pass the Context.%s instead?',
92-
name,
93-
isContextConsumer ? 'Consumer' : 'Provider',
94-
);
78+
if (__DEV__) {
79+
if ('contextType' in (type: any)) {
80+
let isValid =
81+
// Allow null for conditional declaration
82+
contextType === null ||
83+
(contextType !== undefined &&
84+
contextType.$$typeof === REACT_CONTEXT_TYPE &&
85+
contextType._context === undefined); // Not a <Context.Consumer>
86+
87+
if (!isValid && !didWarnAboutInvalidateContextType.has(type)) {
88+
didWarnAboutInvalidateContextType.add(type);
89+
90+
let addendum = '';
91+
if (contextType === undefined) {
92+
addendum =
93+
' However, it is set to undefined. ' +
94+
'This can be caused by a typo or by mixing up named and default imports. ' +
95+
'This can also happen due to a circular dependency, so ' +
96+
'try moving the createContext() call to a separate file.';
97+
} else if (typeof contextType !== 'object') {
98+
addendum = ' However, it is set to a ' + typeof contextType + '.';
99+
} else if (contextType.$$typeof === REACT_PROVIDER_TYPE) {
100+
addendum = ' Did you accidentally pass the Context.Provider instead?';
101+
} else if (contextType._context !== undefined) {
102+
// <Context.Consumer>
103+
addendum = ' Did you accidentally pass the Context.Consumer instead?';
104+
} else {
105+
addendum =
106+
' However, it is set to an object with keys {' +
107+
Object.keys(contextType).join(', ') +
108+
'}.';
95109
}
110+
warningWithoutStack(
111+
false,
112+
'%s defines an invalid contextType. ' +
113+
'contextType should point to the Context object returned by React.createContext().%s',
114+
getComponentName(type) || 'Component',
115+
addendum,
116+
);
96117
}
97118
}
119+
}
120+
if (typeof contextType === 'object' && contextType !== null) {
98121
validateContextBounds(contextType, threadID);
99122
return contextType[threadID];
100123
} else {

packages/react-reconciler/src/ReactFiberClassComponent.js

+37-13
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import shallowEqual from 'shared/shallowEqual';
2424
import getComponentName from 'shared/getComponentName';
2525
import invariant from 'shared/invariant';
2626
import warningWithoutStack from 'shared/warningWithoutStack';
27-
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
27+
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
2828

2929
import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
3030
import {resolveDefaultProps} from './ReactFiberLazyComponent';
@@ -513,27 +513,51 @@ function constructClassInstance(
513513
let unmaskedContext = emptyContextObject;
514514
let context = null;
515515
const contextType = ctor.contextType;
516-
if (typeof contextType === 'object' && contextType !== null) {
517-
if (__DEV__) {
518-
const isContextConsumer =
519-
contextType.$$typeof === REACT_CONTEXT_TYPE &&
520-
contextType._context !== undefined;
521-
if (
522-
(contextType.$$typeof !== REACT_CONTEXT_TYPE || isContextConsumer) &&
523-
!didWarnAboutInvalidateContextType.has(ctor)
524-
) {
516+
517+
if (__DEV__) {
518+
if ('contextType' in ctor) {
519+
let isValid =
520+
// Allow null for conditional declaration
521+
contextType === null ||
522+
(contextType !== undefined &&
523+
contextType.$$typeof === REACT_CONTEXT_TYPE &&
524+
contextType._context === undefined); // Not a <Context.Consumer>
525+
526+
if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) {
525527
didWarnAboutInvalidateContextType.add(ctor);
528+
529+
let addendum = '';
530+
if (contextType === undefined) {
531+
addendum =
532+
' However, it is set to undefined. ' +
533+
'This can be caused by a typo or by mixing up named and default imports. ' +
534+
'This can also happen due to a circular dependency, so ' +
535+
'try moving the createContext() call to a separate file.';
536+
} else if (typeof contextType !== 'object') {
537+
addendum = ' However, it is set to a ' + typeof contextType + '.';
538+
} else if (contextType.$$typeof === REACT_PROVIDER_TYPE) {
539+
addendum = ' Did you accidentally pass the Context.Provider instead?';
540+
} else if (contextType._context !== undefined) {
541+
// <Context.Consumer>
542+
addendum = ' Did you accidentally pass the Context.Consumer instead?';
543+
} else {
544+
addendum =
545+
' However, it is set to an object with keys {' +
546+
Object.keys(contextType).join(', ') +
547+
'}.';
548+
}
526549
warningWithoutStack(
527550
false,
528551
'%s defines an invalid contextType. ' +
529-
'contextType should point to the Context object returned by React.createContext(). ' +
530-
'Did you accidentally pass the Context.%s instead?',
552+
'contextType should point to the Context object returned by React.createContext().%s',
531553
getComponentName(ctor) || 'Component',
532-
isContextConsumer ? 'Consumer' : 'Provider',
554+
addendum,
533555
);
534556
}
535557
}
558+
}
536559

560+
if (typeof contextType === 'object' && contextType !== null) {
537561
context = readContext((contextType: any));
538562
} else {
539563
unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);

packages/react/src/__tests__/ReactContextValidator-test.js

+81
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,87 @@ describe('ReactContextValidator', () => {
578578
);
579579
});
580580

581+
it('should not warn when class contextType is null', () => {
582+
class Foo extends React.Component {
583+
static contextType = null; // Handy for conditional declaration
584+
render() {
585+
return this.context.hello.world;
586+
}
587+
}
588+
expect(() => {
589+
ReactTestUtils.renderIntoDocument(<Foo />);
590+
}).toThrow("Cannot read property 'world' of undefined");
591+
});
592+
593+
it('should warn when class contextType is undefined', () => {
594+
class Foo extends React.Component {
595+
// This commonly happens with circular deps
596+
// https://github.com/facebook/react/issues/13969
597+
static contextType = undefined;
598+
render() {
599+
return this.context.hello.world;
600+
}
601+
}
602+
603+
expect(() => {
604+
expect(() => {
605+
ReactTestUtils.renderIntoDocument(<Foo />);
606+
}).toThrow("Cannot read property 'world' of undefined");
607+
}).toWarnDev(
608+
'Foo defines an invalid contextType. ' +
609+
'contextType should point to the Context object returned by React.createContext(). ' +
610+
'However, it is set to undefined. ' +
611+
'This can be caused by a typo or by mixing up named and default imports. ' +
612+
'This can also happen due to a circular dependency, ' +
613+
'so try moving the createContext() call to a separate file.',
614+
{withoutStack: true},
615+
);
616+
});
617+
618+
it('should warn when class contextType is an object', () => {
619+
class Foo extends React.Component {
620+
// Can happen due to a typo
621+
static contextType = {
622+
x: 42,
623+
y: 'hello',
624+
};
625+
render() {
626+
return this.context.hello.world;
627+
}
628+
}
629+
630+
expect(() => {
631+
expect(() => {
632+
ReactTestUtils.renderIntoDocument(<Foo />);
633+
}).toThrow("Cannot read property 'hello' of undefined");
634+
}).toWarnDev(
635+
'Foo defines an invalid contextType. ' +
636+
'contextType should point to the Context object returned by React.createContext(). ' +
637+
'However, it is set to an object with keys {x, y}.',
638+
{withoutStack: true},
639+
);
640+
});
641+
642+
it('should warn when class contextType is a primitive', () => {
643+
class Foo extends React.Component {
644+
static contextType = 'foo';
645+
render() {
646+
return this.context.hello.world;
647+
}
648+
}
649+
650+
expect(() => {
651+
expect(() => {
652+
ReactTestUtils.renderIntoDocument(<Foo />);
653+
}).toThrow("Cannot read property 'world' of undefined");
654+
}).toWarnDev(
655+
'Foo defines an invalid contextType. ' +
656+
'contextType should point to the Context object returned by React.createContext(). ' +
657+
'However, it is set to a string.',
658+
{withoutStack: true},
659+
);
660+
});
661+
581662
it('should warn if you define contextType on a function component', () => {
582663
const Context = React.createContext();
583664

0 commit comments

Comments
 (0)