Repository: facebook/react
Pull request: experimental_use(context)

<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`.
  2. Run `yarn` in the repository root.
  3. If you've fixed a bug or added code that should be tested, add tests!
  4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
  5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`.
  6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect".
  7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
  8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

  Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
-->

## Summary
Added support for calling `use()` with a React Context in a React Fiber. This simply reads and returns the value of the context. 

## How did you test this change?
New test case `basic use(context)` checks that `use(context)` returns the correct result.

<!--
  Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface.
  How exactly did you verify that your PR solves the issue you wanted to solve?
  If you leave this empty, your PR will very likely be closed.
-->


Changed files:
filename: packages/react-reconciler/src/ReactFiberHooks.new.js
patch: @@ -36,6 +36,7 @@ import {
   enableUseHook,
   enableUseMemoCacheHook,
 } from 'shared/ReactFeatureFlags';
+import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
 
 import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
 import {
@@ -714,68 +715,70 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
 }
 
 function use<T>(usable: Usable<T>): T {
-  if (
-    usable !== null &&
-    typeof usable === 'object' &&
-    typeof usable.then === 'function'
-  ) {
-    // This is a thenable.
-    const thenable: Thenable<T> = (usable: any);
-
-    // Track the position of the thenable within this fiber.
-    const index = thenableIndexCounter;
-    thenableIndexCounter += 1;
-
-    switch (thenable.status) {
-      case 'fulfilled': {
-        const fulfilledValue: T = thenable.value;
-        return fulfilledValue;
-      }
-      case 'rejected': {
-        const rejectedError = thenable.reason;
-        throw rejectedError;
-      }
-      default: {
-        const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
-          index,
-        );
-        if (prevThenableAtIndex !== null) {
-          switch (prevThenableAtIndex.status) {
-            case 'fulfilled': {
-              const fulfilledValue: T = prevThenableAtIndex.value;
-              return fulfilledValue;
-            }
-            case 'rejected': {
-              const rejectedError: mixed = prevThenableAtIndex.reason;
-              throw rejectedError;
-            }
-            default: {
-              // The thenable still hasn't resolved. Suspend with the same
-              // thenable as last time to avoid redundant listeners.
-              throw prevThenableAtIndex;
+  if (usable !== null && typeof usable === 'object') {
+    if (typeof usable.then === 'function') {
+      // This is a thenable.
+      const thenable: Thenable<T> = (usable: any);
+
+      // Track the position of the thenable within this fiber.
+      const index = thenableIndexCounter;
+      thenableIndexCounter += 1;
+
+      switch (thenable.status) {
+        case 'fulfilled': {
+          const fulfilledValue: T = thenable.value;
+          return fulfilledValue;
+        }
+        case 'rejected': {
+          const rejectedError = thenable.reason;
+          throw rejectedError;
+        }
+        default: {
+          const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
+            index,
+          );
+          if (prevThenableAtIndex !== null) {
+            switch (prevThenableAtIndex.status) {
+              case 'fulfilled': {
+                const fulfilledValue: T = prevThenableAtIndex.value;
+                return fulfilledValue;
+              }
+              case 'rejected': {
+                const rejectedError: mixed = prevThenableAtIndex.reason;
+                throw rejectedError;
+              }
+              default: {
+                // The thenable still hasn't resolved. Suspend with the same
+                // thenable as last time to avoid redundant listeners.
+                throw prevThenableAtIndex;
+              }
             }
+          } else {
+            // This is the first time something has been used at this index.
+            // Stash the thenable at the current index so we can reuse it during
+            // the next attempt.
+            trackUsedThenable(thenable, index);
+
+            // Suspend.
+            // TODO: Throwing here is an implementation detail that allows us to
+            // unwind the call stack. But we shouldn't allow it to leak into
+            // userspace. Throw an opaque placeholder value instead of the
+            // actual thenable. If it doesn't get captured by the work loop, log
+            // a warning, because that means something in userspace must have
+            // caught it.
+            throw thenable;
           }
-        } else {
-          // This is the first time something has been used at this index.
-          // Stash the thenable at the current index so we can reuse it during
-          // the next attempt.
-          trackUsedThenable(thenable, index);
-
-          // Suspend.
-          // TODO: Throwing here is an implementation detail that allows us to
-          // unwind the call stack. But we shouldn't allow it to leak into
-          // userspace. Throw an opaque placeholder value instead of the
-          // actual thenable. If it doesn't get captured by the work loop, log
-          // a warning, because that means something in userspace must have
-          // caught it.
-          throw thenable;
         }
       }
+    } else if (
+      usable.$$typeof != null &&
+      usable.$$typeof === REACT_CONTEXT_TYPE
+    ) {
+      const context: ReactContext<T> = (usable: any);
+      return readContext(context);
     }
   }
 
-  // TODO: Add support for Context
-
   // eslint-disable-next-line react-internal/safe-string-coercion
   throw new Error('An unsupported type was passed to use(): ' + String(usable));
 }

filename: packages/react-reconciler/src/ReactFiberHooks.old.js
patch: @@ -36,6 +36,7 @@ import {
   enableUseHook,
   enableUseMemoCacheHook,
 } from 'shared/ReactFeatureFlags';
+import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
 
 import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
 import {
@@ -714,68 +715,70 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
 }
 
 function use<T>(usable: Usable<T>): T {
-  if (
-    usable !== null &&
-    typeof usable === 'object' &&
-    typeof usable.then === 'function'
-  ) {
-    // This is a thenable.
-    const thenable: Thenable<T> = (usable: any);
-
-    // Track the position of the thenable within this fiber.
-    const index = thenableIndexCounter;
-    thenableIndexCounter += 1;
-
-    switch (thenable.status) {
-      case 'fulfilled': {
-        const fulfilledValue: T = thenable.value;
-        return fulfilledValue;
-      }
-      case 'rejected': {
-        const rejectedError = thenable.reason;
-        throw rejectedError;
-      }
-      default: {
-        const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
-          index,
-        );
-        if (prevThenableAtIndex !== null) {
-          switch (prevThenableAtIndex.status) {
-            case 'fulfilled': {
-              const fulfilledValue: T = prevThenableAtIndex.value;
-              return fulfilledValue;
-            }
-            case 'rejected': {
-              const rejectedError: mixed = prevThenableAtIndex.reason;
-              throw rejectedError;
-            }
-            default: {
-              // The thenable still hasn't resolved. Suspend with the same
-              // thenable as last time to avoid redundant listeners.
-              throw prevThenableAtIndex;
+  if (usable !== null && typeof usable === 'object') {
+    if (typeof usable.then === 'function') {
+      // This is a thenable.
+      const thenable: Thenable<T> = (usable: any);
+
+      // Track the position of the thenable within this fiber.
+      const index = thenableIndexCounter;
+      thenableIndexCounter += 1;
+
+      switch (thenable.status) {
+        case 'fulfilled': {
+          const fulfilledValue: T = thenable.value;
+          return fulfilledValue;
+        }
+        case 'rejected': {
+          const rejectedError = thenable.reason;
+          throw rejectedError;
+        }
+        default: {
+          const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
+            index,
+          );
+          if (prevThenableAtIndex !== null) {
+            switch (prevThenableAtIndex.status) {
+              case 'fulfilled': {
+                const fulfilledValue: T = prevThenableAtIndex.value;
+                return fulfilledValue;
+              }
+              case 'rejected': {
+                const rejectedError: mixed = prevThenableAtIndex.reason;
+                throw rejectedError;
+              }
+              default: {
+                // The thenable still hasn't resolved. Suspend with the same
+                // thenable as last time to avoid redundant listeners.
+                throw prevThenableAtIndex;
+              }
             }
+          } else {
+            // This is the first time something has been used at this index.
+            // Stash the thenable at the current index so we can reuse it during
+            // the next attempt.
+            trackUsedThenable(thenable, index);
+
+            // Suspend.
+            // TODO: Throwing here is an implementation detail that allows us to
+            // unwind the call stack. But we shouldn't allow it to leak into
+            // userspace. Throw an opaque placeholder value instead of the
+            // actual thenable. If it doesn't get captured by the work loop, log
+            // a warning, because that means something in userspace must have
+            // caught it.
+            throw thenable;
           }
-        } else {
-          // This is the first time something has been used at this index.
-          // Stash the thenable at the current index so we can reuse it during
-          // the next attempt.
-          trackUsedThenable(thenable, index);
-
-          // Suspend.
-          // TODO: Throwing here is an implementation detail that allows us to
-          // unwind the call stack. But we shouldn't allow it to leak into
-          // userspace. Throw an opaque placeholder value instead of the
-          // actual thenable. If it doesn't get captured by the work loop, log
-          // a warning, because that means something in userspace must have
-          // caught it.
-          throw thenable;
         }
       }
+    } else if (
+      usable.$$typeof != null &&
+      usable.$$typeof === REACT_CONTEXT_TYPE
+    ) {
+      const context: ReactContext<T> = (usable: any);
+      return readContext(context);
     }
   }
 
-  // TODO: Add support for Context
-
   // eslint-disable-next-line react-internal/safe-string-coercion
   throw new Error('An unsupported type was passed to use(): ' + String(usable));
 }

filename: packages/react-reconciler/src/__tests__/ReactWakeable-test.js
patch: @@ -315,4 +315,28 @@ describe('ReactWakeable', () => {
     ]);
     expect(root).toMatchRenderedOutput('Caught an error: Oops!');
   });
+
+  // @gate enableUseHook
+  test('basic use(context)', () => {
+    const ContextA = React.createContext('');
+    const ContextB = React.createContext('B');
+
+    function Sync() {
+      const text = use(ContextA) + use(ContextB);
+      return text;
+    }
+
+    function App() {
+      return (
+        <ContextA.Provider value="A">
+          <Sync />
+        </ContextA.Provider>
+      );
+    }
+
+    const root = ReactNoop.createRoot();
+    root.render(<App />);
+    expect(Scheduler).toFlushWithoutYielding();
+    expect(root).toMatchRenderedOutput('AB');
+  });
 });

filename: packages/shared/ReactTypes.js
patch: @@ -214,4 +214,4 @@ export type StartTransitionOptions = {
 };
 
 // TODO: Add Context support
-export type Usable<T> = Thenable<T>;
+export type Usable<T> = Thenable<T> | ReactContext<T>;