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