@samitouri / QOS-React-2 / commits / 51c07912ac

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) {