Repository: facebook/react
Pull request: Warn when second argument is passed to useContext

This leaves us the ability to later change its meaning. It also warns early avoids confusing cases like `[].map(useContext)` which accidentally supplies `observedBits` (https://github.com/facebook/react/issues/14708#issuecomment-457951903).

Internal `readContext` on the dispatcher doesn't have the warning.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
patch: @@ -613,8 +613,114 @@ describe('ReactDOMServerHooks', () => {
   });
 
   describe('useContext', () => {
+    itThrowsWhenRendering(
+      'if used inside a class component',
+      async render => {
+        const Context = React.createContext({}, () => {});
+        class Counter extends React.Component {
+          render() {
+            let [count] = useContext(Context);
+            return <Text text={count} />;
+          }
+        }
+
+        return render(<Counter />);
+      },
+      'Hooks can only be called inside the body of a function component.',
+    );
+  });
+
+  itRenders(
+    'can use the same context multiple times in the same function',
+    async render => {
+      const Context = React.createContext({foo: 0, bar: 0, baz: 0});
+
+      function Provider(props) {
+        return (
+          <Context.Provider
+            value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
+            {props.children}
+          </Context.Provider>
+        );
+      }
+
+      function FooAndBar() {
+        const {foo} = useContext(Context);
+        const {bar} = useContext(Context);
+        return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
+      }
+
+      function Baz() {
+        const {baz} = useContext(Context);
+        return <Text text={'Baz: ' + baz} />;
+      }
+
+      class Indirection extends React.Component {
+        render() {
+          return this.props.children;
+        }
+      }
+
+      function App(props) {
+        return (
+          <div>
+            <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
+              <Indirection>
+                <Indirection>
+                  <FooAndBar />
+                </Indirection>
+                <Indirection>
+                  <Baz />
+                </Indirection>
+              </Indirection>
+            </Provider>
+          </div>
+        );
+      }
+
+      const domNode = await render(<App foo={1} bar={3} baz={5} />);
+      expect(clearYields()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
+      expect(domNode.childNodes.length).toBe(2);
+      expect(domNode.firstChild.tagName).toEqual('SPAN');
+      expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
+      expect(domNode.lastChild.tagName).toEqual('SPAN');
+      expect(domNode.lastChild.textContent).toEqual('Baz: 5');
+    },
+  );
+
+  itRenders('warns when bitmask is passed to useContext', async render => {
+    let Context = React.createContext('Hi');
+
+    function Foo() {
+      return <span>{useContext(Context, 1)}</span>;
+    }
+
+    const domNode = await render(<Foo />, 1);
+    expect(domNode.textContent).toBe('Hi');
+  });
+
+  describe('useDebugValue', () => {
+    itRenders('is a noop', async render => {
+      function Counter(props) {
+        const debugValue = useDebugValue(123);
+        return <Text text={typeof debugValue} />;
+      }
+
+      const domNode = await render(<Counter />);
+      expect(domNode.textContent).toEqual('undefined');
+    });
+  });
+
+  describe('readContext', () => {
+    function readContext(Context, observedBits) {
+      const dispatcher =
+        React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+          .ReactCurrentDispatcher.current;
+      return dispatcher.readContext(Context, observedBits);
+    }
+
     itRenders(
-      'can use the same context multiple times in the same function',
+      'can read the same context multiple times in the same function',
       async render => {
         const Context = React.createContext(
           {foo: 0, bar: 0, baz: 0},
@@ -643,13 +749,13 @@ describe('ReactDOMServerHooks', () => {
         }
 
         function FooAndBar() {
-          const {foo} = useContext(Context, 0b001);
-          const {bar} = useContext(Context, 0b010);
+          const {foo} = readContext(Context, 0b001);
+          const {bar} = readContext(Context, 0b010);
           return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
         }
 
         function Baz() {
-          const {baz} = useContext(Context, 0b100);
+          const {baz} = readContext(Context, 0b100);
           return <Text text={'Baz: ' + baz} />;
         }
 
@@ -689,43 +795,6 @@ describe('ReactDOMServerHooks', () => {
       },
     );
 
-    itThrowsWhenRendering(
-      'if used inside a class component',
-      async render => {
-        const Context = React.createContext({}, () => {});
-        class Counter extends React.Component {
-          render() {
-            let [count] = useContext(Context);
-            return <Text text={count} />;
-          }
-        }
-
-        return render(<Counter />);
-      },
-      'Hooks can only be called inside the body of a function component.',
-    );
-  });
-
-  describe('useDebugValue', () => {
-    itRenders('is a noop', async render => {
-      function Counter(props) {
-        const debugValue = useDebugValue(123);
-        return <Text text={typeof debugValue} />;
-      }
-
-      const domNode = await render(<Counter />);
-      expect(domNode.textContent).toEqual('undefined');
-    });
-  });
-
-  describe('readContext', () => {
-    function readContext(Context, observedBits) {
-      const dispatcher =
-        React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
-          .ReactCurrentDispatcher.current;
-      return dispatcher.readContext(Context, observedBits);
-    }
-
     itRenders('with a warning inside useMemo and useReducer', async render => {
       const Context = React.createContext(42);
 

filename: packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
patch: @@ -51,23 +51,47 @@ describe('ReactNewContext', () => {
     Context =>
       function Consumer(props) {
         const observedBits = props.unstable_observedBits;
-        const contextValue = useContext(Context, observedBits);
+        let contextValue;
+        expect(() => {
+          contextValue = useContext(Context, observedBits);
+        }).toWarnDev(
+          observedBits !== undefined
+            ? 'useContext() second argument is reserved for future use in React. ' +
+              `Passing it is not supported. You passed: ${observedBits}.`
+            : [],
+        );
         const render = props.children;
         return render(contextValue);
       },
   );
   sharedContextTests('useContext inside forwardRef component', Context =>
     React.forwardRef(function Consumer(props, ref) {
       const observedBits = props.unstable_observedBits;
-      const contextValue = useContext(Context, observedBits);
+      let contextValue;
+      expect(() => {
+        contextValue = useContext(Context, observedBits);
+      }).toWarnDev(
+        observedBits !== undefined
+          ? 'useContext() second argument is reserved for future use in React. ' +
+            `Passing it is not supported. You passed: ${observedBits}.`
+          : [],
+      );
       const render = props.children;
       return render(contextValue);
     }),
   );
   sharedContextTests('useContext inside memoized function component', Context =>
     React.memo(function Consumer(props) {
       const observedBits = props.unstable_observedBits;
-      const contextValue = useContext(Context, observedBits);
+      let contextValue;
+      expect(() => {
+        contextValue = useContext(Context, observedBits);
+      }).toWarnDev(
+        observedBits !== undefined
+          ? 'useContext() second argument is reserved for future use in React. ' +
+            `Passing it is not supported. You passed: ${observedBits}.`
+          : [],
+      );
       const render = props.children;
       return render(contextValue);
     }),
@@ -1300,81 +1324,7 @@ describe('ReactNewContext', () => {
   });
 
   describe('readContext', () => {
-    // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
-    // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
-    // If we bailed out on referential equality, it would be confusing that you
-    // can call this.setState(), but an autobound render callback "blocked" the update.
-    // https://github.com/facebook/react/pull/12470#issuecomment-376917711
-    it('does not bail out if there were no bailouts above it', () => {
-      const Context = React.createContext(0);
-
-      class Consumer extends React.Component {
-        render() {
-          const contextValue = readContext(Context);
-          return this.props.children(contextValue);
-        }
-      }
-
-      class App extends React.Component {
-        state = {
-          text: 'hello',
-        };
-
-        renderConsumer = context => {
-          ReactNoop.yield('App#renderConsumer');
-          return <span prop={this.state.text} />;
-        };
-
-        render() {
-          ReactNoop.yield('App');
-          return (
-            <Context.Provider value={this.props.value}>
-              <Consumer>{this.renderConsumer}</Consumer>
-            </Context.Provider>
-          );
-        }
-      }
-
-      // Initial mount
-      let inst;
-      ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
-      expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
-      expect(ReactNoop.getChildren()).toEqual([span('hello')]);
-
-      // Update
-      inst.setState({text: 'goodbye'});
-      expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
-      expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
-    });
-
-    it('warns when reading context inside render phase class setState updater', () => {
-      const ThemeContext = React.createContext('light');
-
-      class Cls extends React.Component {
-        state = {};
-        render() {
-          this.setState(() => {
-            readContext(ThemeContext);
-          });
-          return null;
-        }
-      }
-
-      ReactNoop.render(<Cls />);
-      expect(ReactNoop.flush).toWarnDev(
-        [
-          'Context can only be read while React is rendering',
-          'Cannot update during an existing state transition',
-        ],
-        {
-          withoutStack: 1,
-        },
-      );
-    });
-  });
-
-  describe('useContext', () => {
-    it('can use the same context multiple times in the same function', () => {
+    it('can read the same context multiple times in the same function', () => {
       const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
         let result = 0;
         if (a.foo !== b.foo) {
@@ -1399,13 +1349,13 @@ describe('ReactNewContext', () => {
       }
 
       function FooAndBar() {
-        const {foo} = useContext(Context, 0b001);
-        const {bar} = useContext(Context, 0b010);
+        const {foo} = readContext(Context, 0b001);
+        const {bar} = readContext(Context, 0b010);
         return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
       }
 
       function Baz() {
-        const {baz} = useContext(Context, 0b100);
+        const {baz} = readContext(Context, 0b100);
         return <Text text={'Baz: ' + baz} />;
       }
 
@@ -1465,6 +1415,96 @@ describe('ReactNewContext', () => {
       ]);
     });
 
+    // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
+    // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
+    // If we bailed out on referential equality, it would be confusing that you
+    // can call this.setState(), but an autobound render callback "blocked" the update.
+    // https://github.com/facebook/react/pull/12470#issuecomment-376917711
+    it('does not bail out if there were no bailouts above it', () => {
+      const Context = React.createContext(0);
+
+      class Consumer extends React.Component {
+        render() {
+          const contextValue = readContext(Context);
+          return this.props.children(contextValue);
+        }
+      }
+
+      class App extends React.Component {
+        state = {
+          text: 'hello',
+        };
+
+        renderConsumer = context => {
+          ReactNoop.yield('App#renderConsumer');
+          return <span prop={this.state.text} />;
+        };
+
+        render() {
+          ReactNoop.yield('App');
+          return (
+            <Context.Provider value={this.props.value}>
+              <Consumer>{this.renderConsumer}</Consumer>
+            </Context.Provider>
+          );
+        }
+      }
+
+      // Initial mount
+      let inst;
+      ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
+      expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
+      expect(ReactNoop.getChildren()).toEqual([span('hello')]);
+
+      // Update
+      inst.setState({text: 'goodbye'});
+      expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
+      expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
+    });
+
+    it('warns when reading context inside render phase class setState updater', () => {
+      const ThemeContext = React.createContext('light');
+
+      class Cls extends React.Component {
+        state = {};
+        render() {
+          this.setState(() => {
+            readContext(ThemeContext);
+          });
+          return null;
+        }
+      }
+
+      ReactNoop.render(<Cls />);
+      expect(ReactNoop.flush).toWarnDev(
+        [
+          'Context can only be read while React is rendering',
+          'Cannot update during an existing state transition',
+        ],
+        {
+          withoutStack: 1,
+        },
+      );
+    });
+  });
+
+  describe('useContext', () => {
+    it('warns on array.map(useContext)', () => {
+      const Context = React.createContext(0);
+      function Foo() {
+        return [Context].map(useContext);
+      }
+      ReactNoop.render(<Foo />);
+      expect(ReactNoop.flush).toWarnDev(
+        'useContext() second argument is reserved for future ' +
+          'use in React. Passing it is not supported. ' +
+          'You passed: 0.\n\n' +
+          'Did you call array.map(useContext)? ' +
+          'Calling Hooks inside a loop is not supported. ' +
+          'Learn more at https://fb.me/rules-of-hooks',
+      );
+    });
+
     it('throws when used in a class component', () => {
       const Context = React.createContext(0);
       class Foo extends React.Component {

filename: packages/react/src/ReactHooks.js
patch: @@ -24,10 +24,23 @@ function resolveDispatcher() {
 
 export function useContext<T>(
   Context: ReactContext<T>,
-  observedBits: number | boolean | void,
+  unstable_observedBits: number | boolean | void,
 ) {
   const dispatcher = resolveDispatcher();
   if (__DEV__) {
+    warning(
+      unstable_observedBits === undefined,
+      'useContext() second argument is reserved for future ' +
+        'use in React. Passing it is not supported. ' +
+        'You passed: %s.%s',
+      unstable_observedBits,
+      typeof unstable_observedBits === 'number' && Array.isArray(arguments[2])
+        ? '\n\nDid you call array.map(useContext)? ' +
+          'Calling Hooks inside a loop is not supported. ' +
+          'Learn more at https://fb.me/rules-of-hooks'
+        : '',
+    );
+
     // TODO: add a more generic warning for invalid values.
     if ((Context: any)._context !== undefined) {
       const realContext = (Context: any)._context;
@@ -48,7 +61,7 @@ export function useContext<T>(
       }
     }
   }
-  return dispatcher.useContext(Context, observedBits);
+  return dispatcher.useContext(Context, unstable_observedBits);
 }
 
 export function useState<S>(initialState: (() => S) | S) {