How does use() work internally in React?

As described on React.dev, use() is a React Hook that lets you read the value of a resource like a Promise or context.

js
import { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
const theme = use(ThemeContext);
// ...
}
js
import { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
const theme = use(ThemeContext);
// ...
}

Looking at the source code, use() internally uses useThenable() for Promise and readContext() for context.

function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
const thenable: Thenable<T> = (usable: any);
return useThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<T> = (usable: any);
return readContext(context);
}
}
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
const thenable: Thenable<T> = (usable: any);
return useThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<T> = (usable: any);
return readContext(context);
}
}
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}

2. Let’s create our own useThenable()

By resource I mean an object that holds its own loading state and it is fairly easy to enable Suspense around it.

In the episode of how lazy() works internally in React, we tried to implement a module loader to do dynamic import as resource,

const createModuleLoader = (load) => {

A general-purpose module loader

return {
module: null,
promise: null,
error: null,
load() {
if (this.module != null) {
-------------------

If already loaded, then just use it

return this.module
}
if (this.error != null) {
throw this.error
}
if (this.promise == null) {
--------------------

If not loaded, and loading not even started,

just kick off the loading

this.promise = load().then((res) => {
// suppose we get an ES module
this.module = res.default
}, (error) => {
this.error = error
})
}
throw this.promise
------------------

If not loaded but loading has started,

then it is not ready, just throw the Promise to suspend

}
}
}
function lazy(load) {
const moduleLoader = createModuleLoader(load)
return function (props) {
const Component = moduleLoader.load()
return <Component {...props}/>
}
}
const createModuleLoader = (load) => {

A general-purpose module loader

return {
module: null,
promise: null,
error: null,
load() {
if (this.module != null) {
-------------------

If already loaded, then just use it

return this.module
}
if (this.error != null) {
throw this.error
}
if (this.promise == null) {
--------------------

If not loaded, and loading not even started,

just kick off the loading

this.promise = load().then((res) => {
// suppose we get an ES module
this.module = res.default
}, (error) => {
this.error = error
})
}
throw this.promise
------------------

If not loaded but loading has started,

then it is not ready, just throw the Promise to suspend

}
}
}
function lazy(load) {
const moduleLoader = createModuleLoader(load)
return function (props) {
const Component = moduleLoader.load()
return <Component {...props}/>
}
}

With above code, the lazily loaded component becomes a resource that properly suspends when loading is not done, try it out 👇

import { Suspense, useState } from 'react';
import lazy from './lazy';

// delay forces loading delay, for demo purpose
const LazyComponent = lazy(() => delay(import('./Component.js')));

export default function App() {
  const [show, setShow] = useState(false)
  return <div>
    <label>
    <input type="checkbox" checked={show} onChange={() => setShow(show => !show)}/>
      show lazy component
    </label>
    <hr/>
    {show ? <Suspense fallback={<p>loading</p>}>
      <LazyComponent/>
    </Suspense> : '-'}
  </div>
}

function delay(promise) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  }).then(() => promise);
}

The module loader accepts argument of loader that return Promises, we can tweak it a little bit to accept Promise directly.

Maybe with a global store to hold the states of the Promises.

const statusStore = new WeakMap()

to NOT prevent the promises from being garbage collected

export function myUseThenable(thenable) {
if (!statusStore.has(thenable)) {
statusStore.set(thenable, {
status: 'pending',
})
}
const entry = statusStore.get(thenable)
if (entry.status === 'pending') {
throw thenable
}

Suspends while loading

if (entry.status === 'fulfilled') {
return entry.value
}
if (entry.status === 'rejected') {
throw entry.error
}
}
const statusStore = new WeakMap()

to NOT prevent the promises from being garbage collected

export function myUseThenable(thenable) {
if (!statusStore.has(thenable)) {
statusStore.set(thenable, {
status: 'pending',
})
}
const entry = statusStore.get(thenable)
if (entry.status === 'pending') {
throw thenable
}

Suspends while loading

if (entry.status === 'fulfilled') {
return entry.value
}
if (entry.status === 'rejected') {
throw entry.error
}
}

Here is a rewrite of the demo with our myUseThenable().

import { Suspense, useState } from 'react';
import myUseThenable from './myUseThenable';

// delay forces loading delay, for demo purpose
const ComponentThenable = delay(import('./Component.js').then(res => res.default))

function LazyComponent() {
  const Component = myUseThenable(ComponentThenable)
  return <Component/>
}

export default function App() {
  const [show, setShow] = useState(false)
  return <div>
    <label>
    <input type="checkbox" checked={show} onChange={() => setShow(show => !show)}/>
      show lazy component
    </label>
    <hr/>
    {show ? <Suspense fallback={<p>loading</p>}>
      <LazyComponent/>
    </Suspense> : '-'}
  </div>
}

function delay(promise) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  }).then(() => promise);
}

The usage looks exactly the same as use(). 🎉

js
function LazyComponent() {
const Component = myUseThenable(ComponentThenable)
return <Component/>
}
js
function LazyComponent() {
const Component = myUseThenable(ComponentThenable)
return <Component/>
}

But the global Promise store looks heavy. Why don’t we just store the status directly on the promise?

export function myUseThenable(thenable) {
if (
typeof thenable === 'object'
&& thenable != null
'then' in thenable
} {
if (thenable.status === 'pending') {
throw thenable
}

Suspend while loading

if (thenable.status === 'fulfilled') {
return thenable.value
}
if (thenable.status === 'rejected') {
throw thenable.error
}
thenable.status = 'pending'
thenable.then((data) => {
thenable.status = 'fulfilled'
thenable.data = data
}, (error) => {
thenable.status = 'rejected'
thenable.error = error
})
} else {
throw new Error('not thenable')
}
}
export function myUseThenable(thenable) {
if (
typeof thenable === 'object'
&& thenable != null
'then' in thenable
} {
if (thenable.status === 'pending') {
throw thenable
}

Suspend while loading

if (thenable.status === 'fulfilled') {
return thenable.value
}
if (thenable.status === 'rejected') {
throw thenable.error
}
thenable.status = 'pending'
thenable.then((data) => {
thenable.status = 'fulfilled'
thenable.data = data
}, (error) => {
thenable.status = 'rejected'
thenable.error = error
})
} else {
throw new Error('not thenable')
}
}

This looks more straightforward now.

3. How does useThenable() work internally?

Let’s dive into the source code.

let thenableIndexCounter: number = 0;

cursor for use-d Promises inside one fiber

let thenableState: ThenableState | null = null;

list of use-d Promises inside one fiber

function useThenable<T>(thenable: Thenable<T>): T {
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
thenableState = createThenableState();
}
const result = trackUsedThenable(thenableState, thenable, index);

For each Fiber, the thenables are stored in an array

if (
currentlyRenderingFiber.alternate === null &&

Initial mount

(workInProgressHook === null

If there is no other hooks called before useThenable()

? currentlyRenderingFiber.memoizedState === null

and there is no hooks waiting to be run

: workInProgressHook.next === null)

Though there are hooks called before useThenable(),

but that is the end from previous render,

Meaning thenable threw or current useThenable() is the last hook

) {
// Initial render, and either this is the first time the component is
// called, or there were no Hooks called after this use() the previous
// time (perhaps because it threw). Subsequent Hook calls should use the
// mount dispatcher.
if (__DEV__) {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
}

This is something we cannot implement by ourselves!

Like what we've explained in How does useState() works internally,

useState() internally use mountState() for initial mount

and updateState() for updates.

But if useThenable() here throws(suspends), then the hooks after it

might not get called for initial mount and later treated as update in the double-rendering

The if conditions are to check that no hooks were called after it

and then forces the hooks to use mount version.

fore more info, refer to the detailed issue report

return result;
}
let thenableIndexCounter: number = 0;

cursor for use-d Promises inside one fiber

let thenableState: ThenableState | null = null;

list of use-d Promises inside one fiber

function useThenable<T>(thenable: Thenable<T>): T {
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
thenableState = createThenableState();
}
const result = trackUsedThenable(thenableState, thenable, index);

For each Fiber, the thenables are stored in an array

if (
currentlyRenderingFiber.alternate === null &&

Initial mount

(workInProgressHook === null

If there is no other hooks called before useThenable()

? currentlyRenderingFiber.memoizedState === null

and there is no hooks waiting to be run

: workInProgressHook.next === null)

Though there are hooks called before useThenable(),

but that is the end from previous render,

Meaning thenable threw or current useThenable() is the last hook

) {
// Initial render, and either this is the first time the component is
// called, or there were no Hooks called after this use() the previous
// time (perhaps because it threw). Subsequent Hook calls should use the
// mount dispatcher.
if (__DEV__) {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
}

This is something we cannot implement by ourselves!

Like what we've explained in How does useState() works internally,

useState() internally use mountState() for initial mount

and updateState() for updates.

But if useThenable() here throws(suspends), then the hooks after it

might not get called for initial mount and later treated as update in the double-rendering

The if conditions are to check that no hooks were called after it

and then forces the hooks to use mount version.

fore more info, refer to the detailed issue report

return result;
}

thenableIndexCounter and thenableState are actually reset in finishRenderingHooks() for each fiber, meaning thenableState is always newly created for each render!

function finishRenderingHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
): void {
{...}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrance.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
thenableIndexCounter = 0;
thenableState = null;
{...}
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
}
if (enableLazyContextPropagation) {
if (current !== null) {
if (!checkIfWorkInProgressReceivedUpdate()) {
// If there were no changes to props or state, we need to check if there
// was a context change. We didn't already do this because there's no
// 1:1 correspondence between dependencies and hooks. Although, because
// there almost always is in the common case (`readContext` is an
// internal API), we could compare in there. OTOH, we only hit this case
// if everything else bails out, so on the whole it might be better to
// keep the comparison out of the common path.
const currentDependencies = current.dependencies;
if (
currentDependencies !== null &&
checkIfContextChanged(currentDependencies)
) {
markWorkInProgressReceivedUpdate();
}
}
}
}
}
function finishRenderingHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
): void {
{...}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrance.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
thenableIndexCounter = 0;
thenableState = null;
{...}
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
}
if (enableLazyContextPropagation) {
if (current !== null) {
if (!checkIfWorkInProgressReceivedUpdate()) {
// If there were no changes to props or state, we need to check if there
// was a context change. We didn't already do this because there's no
// 1:1 correspondence between dependencies and hooks. Although, because
// there almost always is in the common case (`readContext` is an
// internal API), we could compare in there. OTOH, we only hit this case
// if everything else bails out, so on the whole it might be better to
// keep the comparison out of the common path.
const currentDependencies = current.dependencies;
if (
currentDependencies !== null &&
checkIfContextChanged(currentDependencies)
) {
markWorkInProgressReceivedUpdate();
}
}
}
}
}

And thenableState is just an array.

export function createThenableState(): ThenableState {
// The ThenableState is created the first time a component suspends. If it
// suspends again, we'll reuse the same state.
if (__DEV__) {
return {
didWarnAboutUncachedPromise: false,
thenables: [],
};
} else {
return [];
}
}
export function createThenableState(): ThenableState {
// The ThenableState is created the first time a component suspends. If it
// suspends again, we'll reuse the same state.
if (__DEV__) {
return {
didWarnAboutUncachedPromise: false,
thenables: [],
};
} else {
return [];
}
}

The core logic is in trackUsedThenable().

export function trackUsedThenable<T>(
thenableState: ThenableState,
thenable: Thenable<T>,
index: number,
): T {
const trackedThenables = getThenablesFromState(thenableState);
const previous = trackedThenables[index];
if (previous === undefined) {

Since thenableState is newly created on each render,

This means previous will always be undefined?

Kinda yes, but don't forget that we have double-rendering under Strict Mode!

So it is not always true

trackedThenables.push(thenable);
} else {
if (previous !== thenable) {
// Reuse the previous thenable, and drop the new one. We can assume
// they represent the same value, because components are idempotent.
if (__DEV__) {
const thenableStateDev: ThenableStateDev = (thenableState: any);
if (!thenableStateDev.didWarnAboutUncachedPromise) {
{...}
// We should only warn the first time an uncached thenable is
// discovered per component, because if there are multiple, the
// subsequent ones are likely derived from the first.
//
// We track this on the thenableState instead of deduping using the
// component name like we usually do, because in the case of a
// promise-as-React-node, the owner component is likely different from
// the parent that's currently being reconciled. We'd have to track
// the owner using state, which we're trying to move away from. Though
// since this is dev-only, maybe that'd be OK.
//
// However, another benefit of doing it this way is we might
// eventually have a thenableState per memo/Forget boundary instead
// of per component, so this would allow us to have more
// granular warnings.
thenableStateDev.didWarnAboutUncachedPromise = true;
// TODO: This warning should link to a corresponding docs page.
console.error(
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
);
}
}

Actually this is warning is the whole purpose of tracking the promise in an array

It is to make sure we externalize the promise - NO promise creation in render!

// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
thenable.then(noop, noop);
thenable = previous;
}
}
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
default: {
if (typeof thenable.status === 'string') {
{...}
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
thenable.then(noop, noop);
} else {
{...}
// This is an uncached thenable that we haven't seen before.
// Detect infinite ping loops caused by uncached promises.
const root = getWorkInProgressRoot();
if (root !== null && root.shellSuspendCounter > 100) {
// This root has suspended repeatedly in the shell without making any
// progress (i.e. committing something). This is highly suggestive of
// an infinite ping loop, often caused by an accidental Async Client
// Component.
//
// During a transition, we can suspend the work loop until the promise
// to resolve, but this is a sync render, so that's not an option. We
// also can't show a fallback, because none was provided. So our last
// resort is to throw an error.
//
// TODO: Remove this error in a future release. Other ways of handling
// this case include forcing a concurrent render, or putting the whole
// root into offscreen mode.
throw new Error(
'async/await is not yet supported in Client Components, only ' +
'Server Components. This error is often caused by accidentally ' +
"adding `'use client'` to a module that was originally written " +
'for the server.',
);
}
const pendingThenable: PendingThenable<T> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},

success callback

(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},

error callback

);
}
// Check one more time in case the thenable resolved synchronously.
---------------------------------------------------------------

Is this possible? Basically not

but it might be possible if then() is somehow hacked with other implementations

Like this demo I tried.

switch (thenable.status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
const rejectedError = rejectedThenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
}
suspendedThenable = thenable;
throw SuspenseException;

Suspends!

}
}
}
export function trackUsedThenable<T>(
thenableState: ThenableState,
thenable: Thenable<T>,
index: number,
): T {
const trackedThenables = getThenablesFromState(thenableState);
const previous = trackedThenables[index];
if (previous === undefined) {

Since thenableState is newly created on each render,

This means previous will always be undefined?

Kinda yes, but don't forget that we have double-rendering under Strict Mode!

So it is not always true

trackedThenables.push(thenable);
} else {
if (previous !== thenable) {
// Reuse the previous thenable, and drop the new one. We can assume
// they represent the same value, because components are idempotent.
if (__DEV__) {
const thenableStateDev: ThenableStateDev = (thenableState: any);
if (!thenableStateDev.didWarnAboutUncachedPromise) {
{...}
// We should only warn the first time an uncached thenable is
// discovered per component, because if there are multiple, the
// subsequent ones are likely derived from the first.
//
// We track this on the thenableState instead of deduping using the
// component name like we usually do, because in the case of a
// promise-as-React-node, the owner component is likely different from
// the parent that's currently being reconciled. We'd have to track
// the owner using state, which we're trying to move away from. Though
// since this is dev-only, maybe that'd be OK.
//
// However, another benefit of doing it this way is we might
// eventually have a thenableState per memo/Forget boundary instead
// of per component, so this would allow us to have more
// granular warnings.
thenableStateDev.didWarnAboutUncachedPromise = true;
// TODO: This warning should link to a corresponding docs page.
console.error(
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
);
}
}

Actually this is warning is the whole purpose of tracking the promise in an array

It is to make sure we externalize the promise - NO promise creation in render!

// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
thenable.then(noop, noop);
thenable = previous;
}
}
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
default: {
if (typeof thenable.status === 'string') {
{...}
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
thenable.then(noop, noop);
} else {
{...}
// This is an uncached thenable that we haven't seen before.
// Detect infinite ping loops caused by uncached promises.
const root = getWorkInProgressRoot();
if (root !== null && root.shellSuspendCounter > 100) {
// This root has suspended repeatedly in the shell without making any
// progress (i.e. committing something). This is highly suggestive of
// an infinite ping loop, often caused by an accidental Async Client
// Component.
//
// During a transition, we can suspend the work loop until the promise
// to resolve, but this is a sync render, so that's not an option. We
// also can't show a fallback, because none was provided. So our last
// resort is to throw an error.
//
// TODO: Remove this error in a future release. Other ways of handling
// this case include forcing a concurrent render, or putting the whole
// root into offscreen mode.
throw new Error(
'async/await is not yet supported in Client Components, only ' +
'Server Components. This error is often caused by accidentally ' +
"adding `'use client'` to a module that was originally written " +
'for the server.',
);
}
const pendingThenable: PendingThenable<T> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},

success callback

(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},

error callback

);
}
// Check one more time in case the thenable resolved synchronously.
---------------------------------------------------------------

Is this possible? Basically not

but it might be possible if then() is somehow hacked with other implementations

Like this demo I tried.

switch (thenable.status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
const rejectedError = rejectedThenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
}
suspendedThenable = thenable;
throw SuspenseException;

Suspends!

}
}
}

We can see that the overall implementation is quite similar. The complexity of actual use() is that it tries to catch untracked promises to prevent bad code like below.

js
function Component() {
const promise = loadSomething()
// ❌ this is not good
// since this results in different promises on each render
}
const promise = loadSomething()
// ✅ this is good
// since promise is stable across all renders
function Component() {
// ...
}
js
function Component() {
const promise = loadSomething()
// ❌ this is not good
// since this results in different promises on each render
}
const promise = loadSomething()
// ✅ this is good
// since promise is stable across all renders
function Component() {
// ...
}

3.1 caveat: unable to unwrap promise immediately even if it is fulfilled

Here is a demo of using a Promise that is already resolved. Tick the checkbox and we can see that there is loading indicator even if it is initialized as const promise = Promise.resolve('JSer.dev')

import { Suspense, useState, use} from 'react';

const promise = Promise.resolve('JSer.dev')
function LazyComponent() {
  const str = use(promise)
  return <p>{str}</p>
}

export default function App() {
  const [show, setShow] = useState(false)
  return <div>
    <label>
    <input type="checkbox" checked={show} onChange={() => setShow(show => !show)}/>
      show promise value(already fulfilled)
    </label>
    <hr/>
    {show ? <Suspense fallback={<p>loading</p>}>
      <LazyComponent/>
    </Suspense> : '-'}
  </div>
}

function delay(promise) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  }).then(() => promise);
}

The problem is more of Promise itself, we are unable to check its internal states other than attaching callbacks to then() or catch() - both are async.

Looking at the internals of useThenable(), there is actually another round of check on thenable.status after callbacks are setup, so we are actually able to hack then() like below.

const promise = Promise.resolve('hello');
promise.then = (...args) => {
promise.status = 'fulfilled';
promise.value = 'hello';

Trick React to know the fulfilled state earlier

return Promise.prototype.then.call(promise, ...args);
};
const promise = Promise.resolve('hello');
promise.then = (...args) => {
promise.status = 'fulfilled';
promise.value = 'hello';

Trick React to know the fulfilled state earlier

return Promise.prototype.then.call(promise, ...args);
};

With above code, the reveal of Promise value is going to be instant!

import { Suspense, useState, use} from 'react';

const promise = Promise.resolve('JSer.dev')
promise.then = (...args) => {
  promise.status = 'fulfilled';
  promise.value = 'hello';
  return Promise.prototype.then.call(promise, ...args);
};
function LazyComponent() {
  const str = use(promise)
  return <p>{str}</p>
}

export default function App() {
  const [show, setShow] = useState(false)
  return <div>
    <label>
    <input type="checkbox" checked={show} onChange={() => setShow(show => !show)}/>
      show promise value instantly(already fulfilled)
    </label>
    <hr/>
    {show ? <Suspense fallback={<p>loading</p>}>
      <LazyComponent/>
    </Suspense> : '-'}
  </div>
}

function delay(promise) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  }).then(() => promise);
}

4. use() context is equivalent to useContext()

This is simple, ues() uses readContext() internally and useContext() is just an alias for readContext().

function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
const thenable: Thenable<T> = (usable: any);
return useThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<T> = (usable: any);
return readContext(context);
}
}
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
export function readContext<T>(context: ReactContext<T>): T {
return readContextForConsumer(currentlyRenderingFiber, context);
}
...
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
useCallback: mountCallback,
useContext: readContext,
}
function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
const thenable: Thenable<T> = (usable: any);
return useThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<T> = (usable: any);
return readContext(context);
}
}
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
export function readContext<T>(context: ReactContext<T>): T {
return readContextForConsumer(currentlyRenderingFiber, context);
}
...
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
useCallback: mountCallback,
useContext: readContext,
}

For more about context, refer to my post on how does Context work internally in React.

5. use() could be called conditionally

Most useXXX() hooks store and access data from a Fiber node, and the data points form a linked list and hooked to that Fiber.

Take useState() as an example. As explained in how does useState() works internally, there is mountWorkInProgressHook() and updateWorkInProgressHook() to set and get the right data from the linked list, thus call order are important for useState().

But for useThenable() the data is attached to the promise itself, and for readContext() the data is from its closest ancestor Fiber node of the Context Provider.

This is why use() could be called conditionally. useContext() could too, though linter rule warns about it.

6. Summary

use() internally is quite simple, we can actually implement something similar(thought unable to do achieve exactly the same).

Personally I’m not sure if it is a good idea to overload this use() to support Promise and context at the same time(maybe there are going to be more data types supported), because they are totally different internally.

How do you like use()? Hope this post helps you understand React internals better.

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!

😳 Would you like to share my post to more people ?    

❮ Prev: Babel macro:`runCodeForEnvVar()` - run code conditionally on env var.

Next: How does useOptimistic() work internally in React?