@samitouri / QOS-React / commits / f7cb9d2b22

Warn about useContext(Consumer|Provider)

Dan Abramov committed Oct 15, 2018 at 18:31 UTC f7cb9d2b2291b2b4725786105ddd89316711161f
2 files changed +203 -54
packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
+181 -54
@@ -65,16 +65,13 @@ describe('ReactNewContext', () => {
65 return render(contextValue);
66 }),
67 );
68 - sharedContextTests(
69 - 'useContext inside memoized function component',
70 - Context =>
71 - React.memo(function Consumer(props) {
72 - const observedBits = props.unstable_observedBits;
73 - const contextValue = useContext(Context, observedBits);
74 - const render = props.children;
75 - return render(contextValue);
76 - }),
77 - true,
68 + sharedContextTests('useContext inside memoized function component', Context =>
69 + React.memo(function Consumer(props) {
70 + const observedBits = props.unstable_observedBits;
71 + const contextValue = useContext(Context, observedBits);
72 + const render = props.children;
73 + return render(contextValue);
74 + }),
75 );
76 sharedContextTests(
77 'readContext(Context) inside class component',
@@ -88,8 +85,20 @@ describe('ReactNewContext', () => {
85 }
86 },
87 );
88 + sharedContextTests(
89 + 'readContext(Context) inside pure class component',
90 + Context =>
91 + class Consumer extends React.PureComponent {
92 + render() {
93 + const observedBits = this.props.unstable_observedBits;
94 + const contextValue = readContext(Context, observedBits);
95 + const render = this.props.children;
96 + return render(contextValue);
97 + }
98 + },
99 + );
100
92 - function sharedContextTests(label, getConsumer, isPure) {
101 + function sharedContextTests(label, getConsumer) {
102 describe(`reading context with ${label}`, () => {
103 it('simple mount and update', () => {
104 const Context = React.createContext(1);
@@ -874,49 +883,6 @@ describe('ReactNewContext', () => {
883 expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
884 });
885
877 - if (!isPure) {
878 - // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
879 - // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
880 - // If we bailed out on referential equality, it would be confusing that you
881 - // can call this.setState(), but an autobound render callback "blocked" the update.
882 - // https://github.com/facebook/react/pull/12470#issuecomment-376917711
883 - it('consumer does not bail out if there were no bailouts above it', () => {
884 - const Context = React.createContext(0);
885 - const Consumer = getConsumer(Context);
886 -
887 - class App extends React.Component {
888 - state = {
889 - text: 'hello',
890 - };
891 -
892 - renderConsumer = context => {
893 - ReactNoop.yield('App#renderConsumer');
894 - return <span prop={this.state.text} />;
895 - };
896 -
897 - render() {
898 - ReactNoop.yield('App');
899 - return (
900 - <Context.Provider value={this.props.value}>
901 - <Consumer>{this.renderConsumer}</Consumer>
902 - </Context.Provider>
903 - );
904 - }
905 - }
906 -
907 - // Initial mount
908 - let inst;
909 - ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
910 - expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
911 - expect(ReactNoop.getChildren()).toEqual([span('hello')]);
912 -
913 - // Update
914 - inst.setState({text: 'goodbye'});
915 - expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
916 - expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
917 - });
918 - }
919 -
886 // This is a regression case for https://github.com/facebook/react/issues/12389.
887 it('does not run into an infinite loop', () => {
888 const Context = React.createContext(null);
@@ -1260,6 +1226,96 @@ describe('ReactNewContext', () => {
1226 expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
1227 expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 2')]);
1228 });
1229 +
1230 + // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1231 + // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1232 + // If we bailed out on referential equality, it would be confusing that you
1233 + // can call this.setState(), but an autobound render callback "blocked" the update.
1234 + // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1235 + it('consumer does not bail out if there were no bailouts above it', () => {
1236 + const Context = React.createContext(0);
1237 + const Consumer = Context.Consumer;
1238 +
1239 + class App extends React.Component {
1240 + state = {
1241 + text: 'hello',
1242 + };
1243 +
1244 + renderConsumer = context => {
1245 + ReactNoop.yield('App#renderConsumer');
1246 + return <span prop={this.state.text} />;
1247 + };
1248 +
1249 + render() {
1250 + ReactNoop.yield('App');
1251 + return (
1252 + <Context.Provider value={this.props.value}>
1253 + <Consumer>{this.renderConsumer}</Consumer>
1254 + </Context.Provider>
1255 + );
1256 + }
1257 + }
1258 +
1259 + // Initial mount
1260 + let inst;
1261 + ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1262 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1263 + expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1264 +
1265 + // Update
1266 + inst.setState({text: 'goodbye'});
1267 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1268 + expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1269 + });
1270 + });
1271 +
1272 + describe('readContext', () => {
1273 + // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1274 + // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1275 + // If we bailed out on referential equality, it would be confusing that you
1276 + // can call this.setState(), but an autobound render callback "blocked" the update.
1277 + // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1278 + it('does not bail out if there were no bailouts above it', () => {
1279 + const Context = React.createContext(0);
1280 +
1281 + class Consumer extends React.Component {
1282 + render() {
1283 + const contextValue = readContext(Context);
1284 + return this.props.children(contextValue);
1285 + }
1286 + }
1287 +
1288 + class App extends React.Component {
1289 + state = {
1290 + text: 'hello',
1291 + };
1292 +
1293 + renderConsumer = context => {
1294 + ReactNoop.yield('App#renderConsumer');
1295 + return <span prop={this.state.text} />;
1296 + };
1297 +
1298 + render() {
1299 + ReactNoop.yield('App');
1300 + return (
1301 + <Context.Provider value={this.props.value}>
1302 + <Consumer>{this.renderConsumer}</Consumer>
1303 + </Context.Provider>
1304 + );
1305 + }
1306 + }
1307 +
1308 + // Initial mount
1309 + let inst;
1310 + ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1311 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1312 + expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1313 +
1314 + // Update
1315 + inst.setState({text: 'goodbye'});
1316 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1317 + expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1318 + });
1319 });
1320
1321 describe('useContext', () => {
@@ -1366,6 +1422,77 @@ describe('ReactNewContext', () => {
1422 'Hooks can only be called inside the body of a function component.',
1423 );
1424 });
1425 +
1426 + it('warns when passed a consumer', () => {
1427 + const Context = React.createContext(0);
1428 + function Foo() {
1429 + return useContext(Context.Consumer);
1430 + }
1431 + ReactNoop.render(<Foo />);
1432 + expect(ReactNoop.flush).toWarnDev(
1433 + 'Calling useContext(Context.Consumer) is not supported, may cause bugs, ' +
1434 + 'and will be removed in a future major release. ' +
1435 + 'Did you mean to call useContext(Context) instead?',
1436 + );
1437 + });
1438 +
1439 + it('warns when passed a provider', () => {
1440 + const Context = React.createContext(0);
1441 + function Foo() {
1442 + useContext(Context.Provider);
1443 + return null;
1444 + }
1445 + ReactNoop.render(<Foo />);
1446 + expect(ReactNoop.flush).toWarnDev(
1447 + 'Calling useContext(Context.Provider) is not supported. ' +
1448 + 'Did you mean to call useContext(Context) instead?',
1449 + );
1450 + });
1451 +
1452 + // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1453 + // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1454 + // If we bailed out on referential equality, it would be confusing that you
1455 + // can call this.setState(), but an autobound render callback "blocked" the update.
1456 + // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1457 + it('does not bail out if there were no bailouts above it', () => {
1458 + const Context = React.createContext(0);
1459 +
1460 + function Consumer({children}) {
1461 + const contextValue = useContext(Context);
1462 + return children(contextValue);
1463 + }
1464 +
1465 + class App extends React.Component {
1466 + state = {
1467 + text: 'hello',
1468 + };
1469 +
1470 + renderConsumer = context => {
1471 + ReactNoop.yield('App#renderConsumer');
1472 + return <span prop={this.state.text} />;
1473 + };
1474 +
1475 + render() {
1476 + ReactNoop.yield('App');
1477 + return (
1478 + <Context.Provider value={this.props.value}>
1479 + <Consumer>{this.renderConsumer}</Consumer>
1480 + </Context.Provider>
1481 + );
1482 + }
1483 + }
1484 +
1485 + // Initial mount
1486 + let inst;
1487 + ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1488 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1489 + expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1490 +
1491 + // Update
1492 + inst.setState({text: 'goodbye'});
1493 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1494 + expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1495 + });
1496 });
1497
1498 it('unwinds after errors in complete phase', () => {
packages/react/src/ReactHooks.js
+22
@@ -9,6 +9,7 @@
9
10 import type {ReactContext} from 'shared/ReactTypes';
11 import invariant from 'shared/invariant';
12 +import warning from 'shared/warning';
13
14 import ReactCurrentOwner from './ReactCurrentOwner';
15
@@ -26,6 +27,27 @@ export function useContext<T>(
27 observedBits: number | boolean | void,
28 ) {
29 const dispatcher = resolveDispatcher();
30 + if (__DEV__) {
31 + // TODO: add a more generic warning for invalid values.
32 + if ((Context: any)._context !== undefined) {
33 + const realContext = (Context: any)._context;
34 + // Don't deduplicate because this legitimately causes bugs
35 + // and nobody should be using this in existing code.
36 + if (realContext.Consumer === Context) {
37 + warning(
38 + false,
39 + 'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' +
40 + 'removed in a future major release. Did you mean to call useContext(Context) instead?',
41 + );
42 + } else if (realContext.Provider === Context) {
43 + warning(
44 + false,
45 + 'Calling useContext(Context.Provider) is not supported. ' +
46 + 'Did you mean to call useContext(Context) instead?',
47 + );
48 + }
49 + }
50 + }
51 return dispatcher.useContext(Context, observedBits);
52 }
53