Warn when second argument is passed to useCallback (#14729)
Dan Abramov committed
Jan 31, 2019 at 13:56 UTC
51c07912ac047d076e15e730eda1f4634cfe144f
3 files changed
+246
-124
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+110
-41
@@ -613,8 +613,114 @@ describe('ReactDOMServerHooks', () => {
613
});
614
615
describe('useContext', () => {
616
+ itThrowsWhenRendering(
617
+ 'if used inside a class component',
618
+ async render => {
619
+ const Context = React.createContext({}, () => {});
620
+ class Counter extends React.Component {
621
+ render() {
622
+ let [count] = useContext(Context);
623
+ return <Text text={count} />;
624
+ }
625
+ }
626
+
627
+ return render(<Counter />);
628
+ },
629
+ 'Hooks can only be called inside the body of a function component.',
630
+ );
631
+ });
632
+
633
+ itRenders(
634
+ 'can use the same context multiple times in the same function',
635
+ async render => {
636
+ const Context = React.createContext({foo: 0, bar: 0, baz: 0});
637
+
638
+ function Provider(props) {
639
+ return (
640
+ <Context.Provider
641
+ value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
642
+ {props.children}
643
+ </Context.Provider>
644
+ );
645
+ }
646
+
647
+ function FooAndBar() {
648
+ const {foo} = useContext(Context);
649
+ const {bar} = useContext(Context);
650
+ return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
651
+ }
652
+
653
+ function Baz() {
654
+ const {baz} = useContext(Context);
655
+ return <Text text={'Baz: ' + baz} />;
656
+ }
657
+
658
+ class Indirection extends React.Component {
659
+ render() {
660
+ return this.props.children;
661
+ }
662
+ }
663
+
664
+ function App(props) {
665
+ return (
666
+ <div>
667
+ <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
668
+ <Indirection>
669
+ <Indirection>
670
+ <FooAndBar />
671
+ </Indirection>
672
+ <Indirection>
673
+ <Baz />
674
+ </Indirection>
675
+ </Indirection>
676
+ </Provider>
677
+ </div>
678
+ );
679
+ }
680
+
681
+ const domNode = await render(<App foo={1} bar={3} baz={5} />);
682
+ expect(clearYields()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
683
+ expect(domNode.childNodes.length).toBe(2);
684
+ expect(domNode.firstChild.tagName).toEqual('SPAN');
685
+ expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
686
+ expect(domNode.lastChild.tagName).toEqual('SPAN');
687
+ expect(domNode.lastChild.textContent).toEqual('Baz: 5');
688
+ },
689
+ );
690
+
691
+ itRenders('warns when bitmask is passed to useContext', async render => {
692
+ let Context = React.createContext('Hi');
693
+
694
+ function Foo() {
695
+ return <span>{useContext(Context, 1)}</span>;
696
+ }
697
+
698
+ const domNode = await render(<Foo />, 1);
699
+ expect(domNode.textContent).toBe('Hi');
700
+ });
701
+
702
+ describe('useDebugValue', () => {
703
+ itRenders('is a noop', async render => {
704
+ function Counter(props) {
705
+ const debugValue = useDebugValue(123);
706
+ return <Text text={typeof debugValue} />;
707
+ }
708
+
709
+ const domNode = await render(<Counter />);
710
+ expect(domNode.textContent).toEqual('undefined');
711
+ });
712
+ });
713
+
714
+ describe('readContext', () => {
715
+ function readContext(Context, observedBits) {
716
+ const dispatcher =
717
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
718
+ .ReactCurrentDispatcher.current;
719
+ return dispatcher.readContext(Context, observedBits);
720
+ }
721
+
722
itRenders(
617
- 'can use the same context multiple times in the same function',
723
+ 'can read the same context multiple times in the same function',
724
async render => {
725
const Context = React.createContext(
726
{foo: 0, bar: 0, baz: 0},
@@ -643,13 +749,13 @@ describe('ReactDOMServerHooks', () => {
749
}
750
751
function FooAndBar() {
646
- const {foo} = useContext(Context, 0b001);
647
- const {bar} = useContext(Context, 0b010);
752
+ const {foo} = readContext(Context, 0b001);
753
+ const {bar} = readContext(Context, 0b010);
754
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
755
}
756
757
function Baz() {
652
- const {baz} = useContext(Context, 0b100);
758
+ const {baz} = readContext(Context, 0b100);
759
return <Text text={'Baz: ' + baz} />;
760
}
761
@@ -689,43 +795,6 @@ describe('ReactDOMServerHooks', () => {
795
},
796
);
797
692
- itThrowsWhenRendering(
693
- 'if used inside a class component',
694
- async render => {
695
- const Context = React.createContext({}, () => {});
696
- class Counter extends React.Component {
697
- render() {
698
- let [count] = useContext(Context);
699
- return <Text text={count} />;
700
- }
701
- }
702
-
703
- return render(<Counter />);
704
- },
705
- 'Hooks can only be called inside the body of a function component.',
706
- );
707
- });
708
-
709
- describe('useDebugValue', () => {
710
- itRenders('is a noop', async render => {
711
- function Counter(props) {
712
- const debugValue = useDebugValue(123);
713
- return <Text text={typeof debugValue} />;
714
- }
715
-
716
- const domNode = await render(<Counter />);
717
- expect(domNode.textContent).toEqual('undefined');
718
- });
719
- });
720
-
721
- describe('readContext', () => {
722
- function readContext(Context, observedBits) {
723
- const dispatcher =
724
- React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
725
- .ReactCurrentDispatcher.current;
726
- return dispatcher.readContext(Context, observedBits);
727
- }
728
-
798
itRenders('with a warning inside useMemo and useReducer', async render => {
799
const Context = React.createContext(42);
800
packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
+121
-81
@@ -51,7 +51,15 @@ describe('ReactNewContext', () => {
51
Context =>
52
function Consumer(props) {
53
const observedBits = props.unstable_observedBits;
54
- const contextValue = useContext(Context, observedBits);
54
+ let contextValue;
55
+ expect(() => {
56
+ contextValue = useContext(Context, observedBits);
57
+ }).toWarnDev(
58
+ observedBits !== undefined
59
+ ? 'useContext() second argument is reserved for future use in React. ' +
60
+ `Passing it is not supported. You passed: ${observedBits}.`
61
+ : [],
62
+ );
63
const render = props.children;
64
return render(contextValue);
65
},
@@ -59,7 +67,15 @@ describe('ReactNewContext', () => {
67
sharedContextTests('useContext inside forwardRef component', Context =>
68
React.forwardRef(function Consumer(props, ref) {
69
const observedBits = props.unstable_observedBits;
62
- const contextValue = useContext(Context, observedBits);
70
+ let contextValue;
71
+ expect(() => {
72
+ contextValue = useContext(Context, observedBits);
73
+ }).toWarnDev(
74
+ observedBits !== undefined
75
+ ? 'useContext() second argument is reserved for future use in React. ' +
76
+ `Passing it is not supported. You passed: ${observedBits}.`
77
+ : [],
78
+ );
79
const render = props.children;
80
return render(contextValue);
81
}),
@@ -67,7 +83,15 @@ describe('ReactNewContext', () => {
83
sharedContextTests('useContext inside memoized function component', Context =>
84
React.memo(function Consumer(props) {
85
const observedBits = props.unstable_observedBits;
70
- const contextValue = useContext(Context, observedBits);
86
+ let contextValue;
87
+ expect(() => {
88
+ contextValue = useContext(Context, observedBits);
89
+ }).toWarnDev(
90
+ observedBits !== undefined
91
+ ? 'useContext() second argument is reserved for future use in React. ' +
92
+ `Passing it is not supported. You passed: ${observedBits}.`
93
+ : [],
94
+ );
95
const render = props.children;
96
return render(contextValue);
97
}),
@@ -1300,81 +1324,7 @@ describe('ReactNewContext', () => {
1324
});
1325
1326
describe('readContext', () => {
1303
- // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1304
- // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1305
- // If we bailed out on referential equality, it would be confusing that you
1306
- // can call this.setState(), but an autobound render callback "blocked" the update.
1307
- // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1308
- it('does not bail out if there were no bailouts above it', () => {
1309
- const Context = React.createContext(0);
1310
-
1311
- class Consumer extends React.Component {
1312
- render() {
1313
- const contextValue = readContext(Context);
1314
- return this.props.children(contextValue);
1315
- }
1316
- }
1317
-
1318
- class App extends React.Component {
1319
- state = {
1320
- text: 'hello',
1321
- };
1322
-
1323
- renderConsumer = context => {
1324
- ReactNoop.yield('App#renderConsumer');
1325
- return <span prop={this.state.text} />;
1326
- };
1327
-
1328
- render() {
1329
- ReactNoop.yield('App');
1330
- return (
1331
- <Context.Provider value={this.props.value}>
1332
- <Consumer>{this.renderConsumer}</Consumer>
1333
- </Context.Provider>
1334
- );
1335
- }
1336
- }
1337
-
1338
- // Initial mount
1339
- let inst;
1340
- ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1341
- expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1342
- expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1343
-
1344
- // Update
1345
- inst.setState({text: 'goodbye'});
1346
- expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1347
- expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1348
- });
1349
-
1350
- it('warns when reading context inside render phase class setState updater', () => {
1351
- const ThemeContext = React.createContext('light');
1352
-
1353
- class Cls extends React.Component {
1354
- state = {};
1355
- render() {
1356
- this.setState(() => {
1357
- readContext(ThemeContext);
1358
- });
1359
- return null;
1360
- }
1361
- }
1362
-
1363
- ReactNoop.render(<Cls />);
1364
- expect(ReactNoop.flush).toWarnDev(
1365
- [
1366
- 'Context can only be read while React is rendering',
1367
- 'Cannot update during an existing state transition',
1368
- ],
1369
- {
1370
- withoutStack: 1,
1371
- },
1372
- );
1373
- });
1374
- });
1375
-
1376
- describe('useContext', () => {
1377
- it('can use the same context multiple times in the same function', () => {
1327
+ it('can read the same context multiple times in the same function', () => {
1328
const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
1329
let result = 0;
1330
if (a.foo !== b.foo) {
@@ -1399,13 +1349,13 @@ describe('ReactNewContext', () => {
1349
}
1350
1351
function FooAndBar() {
1402
- const {foo} = useContext(Context, 0b001);
1403
- const {bar} = useContext(Context, 0b010);
1352
+ const {foo} = readContext(Context, 0b001);
1353
+ const {bar} = readContext(Context, 0b010);
1354
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1355
}
1356
1357
function Baz() {
1408
- const {baz} = useContext(Context, 0b100);
1358
+ const {baz} = readContext(Context, 0b100);
1359
return <Text text={'Baz: ' + baz} />;
1360
}
1361
@@ -1465,6 +1415,96 @@ describe('ReactNewContext', () => {
1415
]);
1416
});
1417
1418
+ // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1419
+ // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1420
+ // If we bailed out on referential equality, it would be confusing that you
1421
+ // can call this.setState(), but an autobound render callback "blocked" the update.
1422
+ // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1423
+ it('does not bail out if there were no bailouts above it', () => {
1424
+ const Context = React.createContext(0);
1425
+
1426
+ class Consumer extends React.Component {
1427
+ render() {
1428
+ const contextValue = readContext(Context);
1429
+ return this.props.children(contextValue);
1430
+ }
1431
+ }
1432
+
1433
+ class App extends React.Component {
1434
+ state = {
1435
+ text: 'hello',
1436
+ };
1437
+
1438
+ renderConsumer = context => {
1439
+ ReactNoop.yield('App#renderConsumer');
1440
+ return <span prop={this.state.text} />;
1441
+ };
1442
+
1443
+ render() {
1444
+ ReactNoop.yield('App');
1445
+ return (
1446
+ <Context.Provider value={this.props.value}>
1447
+ <Consumer>{this.renderConsumer}</Consumer>
1448
+ </Context.Provider>
1449
+ );
1450
+ }
1451
+ }
1452
+
1453
+ // Initial mount
1454
+ let inst;
1455
+ ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1456
+ expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1457
+ expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1458
+
1459
+ // Update
1460
+ inst.setState({text: 'goodbye'});
1461
+ expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1462
+ expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1463
+ });
1464
+
1465
+ it('warns when reading context inside render phase class setState updater', () => {
1466
+ const ThemeContext = React.createContext('light');
1467
+
1468
+ class Cls extends React.Component {
1469
+ state = {};
1470
+ render() {
1471
+ this.setState(() => {
1472
+ readContext(ThemeContext);
1473
+ });
1474
+ return null;
1475
+ }
1476
+ }
1477
+
1478
+ ReactNoop.render(<Cls />);
1479
+ expect(ReactNoop.flush).toWarnDev(
1480
+ [
1481
+ 'Context can only be read while React is rendering',
1482
+ 'Cannot update during an existing state transition',
1483
+ ],
1484
+ {
1485
+ withoutStack: 1,
1486
+ },
1487
+ );
1488
+ });
1489
+ });
1490
+
1491
+ describe('useContext', () => {
1492
+ it('warns on array.map(useContext)', () => {
1493
+ const Context = React.createContext(0);
1494
+ function Foo() {
1495
+ return [Context].map(useContext);
1496
+ }
1497
+ ReactNoop.render(<Foo />);
1498
+ expect(ReactNoop.flush).toWarnDev(
1499
+ 'useContext() second argument is reserved for future ' +
1500
+ 'use in React. Passing it is not supported. ' +
1501
+ 'You passed: 0.\n\n' +
1502
+ 'Did you call array.map(useContext)? ' +
1503
+ 'Calling Hooks inside a loop is not supported. ' +
1504
+ 'Learn more at https://fb.me/rules-of-hooks',
1505
+ );
1506
+ });
1507
+
1508
it('throws when used in a class component', () => {
1509
const Context = React.createContext(0);
1510
class Foo extends React.Component {
packages/react/src/ReactHooks.js
+15
-2
@@ -24,10 +24,23 @@ function resolveDispatcher() {
24
25
export function useContext<T>(
26
Context: ReactContext<T>,
27
- observedBits: number | boolean | void,
27
+ unstable_observedBits: number | boolean | void,
28
) {
29
const dispatcher = resolveDispatcher();
30
if (__DEV__) {
31
+ warning(
32
+ unstable_observedBits === undefined,
33
+ 'useContext() second argument is reserved for future ' +
34
+ 'use in React. Passing it is not supported. ' +
35
+ 'You passed: %s.%s',
36
+ unstable_observedBits,
37
+ typeof unstable_observedBits === 'number' && Array.isArray(arguments[2])
38
+ ? '\n\nDid you call array.map(useContext)? ' +
39
+ 'Calling Hooks inside a loop is not supported. ' +
40
+ 'Learn more at https://fb.me/rules-of-hooks'
41
+ : '',
42
+ );
43
+
44
// TODO: add a more generic warning for invalid values.
45
if ((Context: any)._context !== undefined) {
46
const realContext = (Context: any)._context;
@@ -48,7 +61,7 @@ export function useContext<T>(
61
}
62
}
63
}
51
- return dispatcher.useContext(Context, observedBits);
64
+ return dispatcher.useContext(Context, unstable_observedBits);
65
}
66
67
export function useState<S>(initialState: (() => S) | S) {