From bb970ed1d3359209ce574ac03b84f9a271100785 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 11 Aug 2022 16:24:13 -0400 Subject: [PATCH] use(promise) Adds experimental support to Fiber for unwrapping the value of a promise inside a component. It is not yet implemented for Server Components, but that is planned. If promise has already resolved, the value can be unwrapped "immediately" without showing a fallback. The trick we use to implement this is to yield to the main thread (literally suspending the work loop), wait for the microtask queue to drain, then check if the promise resolved in the meantime. If so, we can resume the last attempted fiber without unwinding the stack. This functionality was implemented in previous commits. Another feature is that the promises do not need to be cached between attempts. Because we assume idempotent execution of components, React will track the promises that were used during the previous attempt and reuse the result. You shouldn't rely on this property, but during initial render it mostly just works. Updates are trickier, though, because if you used an uncached promise, we have no way of knowing whether the underlying data has changed, so we have to unwrap the promise every time. It will still work, but it's inefficient and can lead to unnecessary fallbacks if it happens during a discrete update. When we implement this for Server Components, this will be less of an issue because there are no updates in that environment. However, it's still better for performance to cache data requests, so the same principles largely apply. The intention is that this will eventually be the only supported way to suspend on arbitrary promises. Throwing a promise directly will be deprecated. --- .../src/ReactFiberHooks.new.js | 96 ++++++++++++++++++- .../src/ReactFiberHooks.old.js | 96 ++++++++++++++++++- .../src/ReactFiberWakeable.new.js | 40 +++++++- .../src/ReactFiberWakeable.old.js | 40 +++++++- .../src/ReactFiberWorkLoop.new.js | 14 ++- .../src/ReactFiberWorkLoop.old.js | 14 ++- .../src/__tests__/ReactWakeable-test.js | 71 ++++++++++++++ packages/shared/ReactTypes.js | 39 +++++--- scripts/error-codes/codes.json | 3 +- 9 files changed, 385 insertions(+), 28 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 615dee30e6631..13636ae9571b4 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -14,6 +14,10 @@ import type { ReactContext, StartTransitionOptions, Usable, + Thenable, + PendingThenable, + FulfilledThenable, + RejectedThenable, } from 'shared/ReactTypes'; import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; @@ -121,6 +125,10 @@ import { } from './ReactFiberConcurrentUpdates.new'; import {getTreeId} from './ReactFiberTreeContext.new'; import {now} from './Scheduler'; +import { + trackSuspendedThenable, + getSuspendedThenable, +} from './ReactFiberWakeable.new'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -206,6 +214,9 @@ let didScheduleRenderPhaseUpdate: boolean = false; let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false; // Counts the number of useId hooks in this component. let localIdCounter: number = 0; +// Counts number of `use`-d thenables +let thenableIndexCounter: number = 0; + // Used for ids that are generated completely client-side (i.e. not during // hydration). This counter is global, so client ids are not stable across // render attempts. @@ -404,6 +415,7 @@ export function renderWithHooks( // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; + // thenableIndexCounter = 0; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. @@ -442,6 +454,7 @@ export function renderWithHooks( do { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; + thenableIndexCounter = 0; if (numberOfReRenders >= RE_RENDER_LIMIT) { throw new Error( @@ -525,6 +538,7 @@ export function renderWithHooks( didScheduleRenderPhaseUpdate = false; // This is reset by checkDidRenderIdHook // localIdCounter = 0; + thenableIndexCounter = 0; if (didRenderTooFewHooks) { throw new Error( @@ -632,6 +646,7 @@ export function resetHooksAfterThrow(): void { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; + thenableIndexCounter = 0; } function mountWorkInProgressHook(): Hook { @@ -724,7 +739,86 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue { } function use(usable: Usable): T { - throw new Error('Not implemented.'); + if ( + usable !== null && + typeof usable === 'object' && + typeof usable.then === 'function' + ) { + // This is a thenable. + const thenable: Thenable = (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 | null = getSuspendedThenable( + 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. + trackSuspendedThenable(thenable, index); + + // Assume that if the status is `pending`, there's already a listener + // that will set it to `fulfilled` or `rejected`. + // TODO: We could do this in `pingSuspendedRoot` instead. + if (thenable.status !== 'pending') { + const pendingThenable: PendingThenable = (thenable: any); + pendingThenable.status = 'pending'; + pendingThenable.then( + fulfilledValue => { + if (thenable.status === 'pending') { + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = fulfilledValue; + } + }, + (error: mixed) => { + if (thenable.status === 'pending') { + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; + } + }, + ); + } + throw thenable; + } + } + } + } + + // 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)); } function basicStateReducer(state: S, action: BasicStateAction): S { diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index a576c85052c6b..8407fafbd3bbb 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -14,6 +14,10 @@ import type { ReactContext, StartTransitionOptions, Usable, + Thenable, + PendingThenable, + FulfilledThenable, + RejectedThenable, } from 'shared/ReactTypes'; import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; @@ -121,6 +125,10 @@ import { } from './ReactFiberConcurrentUpdates.old'; import {getTreeId} from './ReactFiberTreeContext.old'; import {now} from './Scheduler'; +import { + trackSuspendedThenable, + getSuspendedThenable, +} from './ReactFiberWakeable.old'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -206,6 +214,9 @@ let didScheduleRenderPhaseUpdate: boolean = false; let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false; // Counts the number of useId hooks in this component. let localIdCounter: number = 0; +// Counts number of `use`-d thenables +let thenableIndexCounter: number = 0; + // Used for ids that are generated completely client-side (i.e. not during // hydration). This counter is global, so client ids are not stable across // render attempts. @@ -404,6 +415,7 @@ export function renderWithHooks( // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; + // thenableIndexCounter = 0; // TODO Warn if no hooks are used at all during mount, then some are used during update. // Currently we will identify the update render as a mount because memoizedState === null. @@ -442,6 +454,7 @@ export function renderWithHooks( do { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; + thenableIndexCounter = 0; if (numberOfReRenders >= RE_RENDER_LIMIT) { throw new Error( @@ -525,6 +538,7 @@ export function renderWithHooks( didScheduleRenderPhaseUpdate = false; // This is reset by checkDidRenderIdHook // localIdCounter = 0; + thenableIndexCounter = 0; if (didRenderTooFewHooks) { throw new Error( @@ -632,6 +646,7 @@ export function resetHooksAfterThrow(): void { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; + thenableIndexCounter = 0; } function mountWorkInProgressHook(): Hook { @@ -724,7 +739,86 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue { } function use(usable: Usable): T { - throw new Error('Not implemented.'); + if ( + usable !== null && + typeof usable === 'object' && + typeof usable.then === 'function' + ) { + // This is a thenable. + const thenable: Thenable = (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 | null = getSuspendedThenable( + 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. + trackSuspendedThenable(thenable, index); + + // Assume that if the status is `pending`, there's already a listener + // that will set it to `fulfilled` or `rejected`. + // TODO: We could do this in `pingSuspendedRoot` instead. + if (thenable.status !== 'pending') { + const pendingThenable: PendingThenable = (thenable: any); + pendingThenable.status = 'pending'; + pendingThenable.then( + fulfilledValue => { + if (thenable.status === 'pending') { + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = fulfilledValue; + } + }, + (error: mixed) => { + if (thenable.status === 'pending') { + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; + } + }, + ); + } + throw thenable; + } + } + } + } + + // 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)); } function basicStateReducer(state: S, action: BasicStateAction): S { diff --git a/packages/react-reconciler/src/ReactFiberWakeable.new.js b/packages/react-reconciler/src/ReactFiberWakeable.new.js index 589d61eae814a..67cd0ebd93b4a 100644 --- a/packages/react-reconciler/src/ReactFiberWakeable.new.js +++ b/packages/react-reconciler/src/ReactFiberWakeable.new.js @@ -7,12 +7,15 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type {Wakeable, Thenable} from 'shared/ReactTypes'; let suspendedWakeable: Wakeable | null = null; let wasPinged = false; let adHocSuspendCount: number = 0; +let suspendedThenables: Array | void> | null = null; +let lastUsedThenable: Thenable | null = null; + const MAX_AD_HOC_SUSPEND_COUNT = 50; export function suspendedWakeableWasPinged() { @@ -20,7 +23,12 @@ export function suspendedWakeableWasPinged() { } export function trackSuspendedWakeable(wakeable: Wakeable) { - adHocSuspendCount++; + if (wakeable !== lastUsedThenable) { + // If this wakeable was not just `use`-d, it must be an ad hoc wakeable + // that was thrown by an older Suspense implementation. Keep a count of + // these so that we can detect an infinite ping loop. + adHocSuspendCount++; + } suspendedWakeable = wakeable; } @@ -35,10 +43,15 @@ export function attemptToPingSuspendedWakeable(wakeable: Wakeable) { return false; } -export function resetWakeableState() { +export function resetWakeableStateAfterEachAttempt() { suspendedWakeable = null; wasPinged = false; adHocSuspendCount = 0; + lastUsedThenable = null; +} + +export function resetThenableStateOnCompletion() { + suspendedThenables = null; } export function throwIfInfinitePingLoopDetected() { @@ -48,3 +61,24 @@ export function throwIfInfinitePingLoopDetected() { // the render phase so that it gets the component stack. } } + +export function trackSuspendedThenable( + thenable: Thenable, + index: number, +) { + if (suspendedThenables === null) { + suspendedThenables = []; + } + suspendedThenables[index] = thenable; + lastUsedThenable = thenable; +} + +export function getSuspendedThenable(index: number): Thenable | null { + if (suspendedThenables !== null) { + const thenable = suspendedThenables[index]; + if (thenable !== undefined) { + return thenable; + } + } + return null; +} diff --git a/packages/react-reconciler/src/ReactFiberWakeable.old.js b/packages/react-reconciler/src/ReactFiberWakeable.old.js index 589d61eae814a..67cd0ebd93b4a 100644 --- a/packages/react-reconciler/src/ReactFiberWakeable.old.js +++ b/packages/react-reconciler/src/ReactFiberWakeable.old.js @@ -7,12 +7,15 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type {Wakeable, Thenable} from 'shared/ReactTypes'; let suspendedWakeable: Wakeable | null = null; let wasPinged = false; let adHocSuspendCount: number = 0; +let suspendedThenables: Array | void> | null = null; +let lastUsedThenable: Thenable | null = null; + const MAX_AD_HOC_SUSPEND_COUNT = 50; export function suspendedWakeableWasPinged() { @@ -20,7 +23,12 @@ export function suspendedWakeableWasPinged() { } export function trackSuspendedWakeable(wakeable: Wakeable) { - adHocSuspendCount++; + if (wakeable !== lastUsedThenable) { + // If this wakeable was not just `use`-d, it must be an ad hoc wakeable + // that was thrown by an older Suspense implementation. Keep a count of + // these so that we can detect an infinite ping loop. + adHocSuspendCount++; + } suspendedWakeable = wakeable; } @@ -35,10 +43,15 @@ export function attemptToPingSuspendedWakeable(wakeable: Wakeable) { return false; } -export function resetWakeableState() { +export function resetWakeableStateAfterEachAttempt() { suspendedWakeable = null; wasPinged = false; adHocSuspendCount = 0; + lastUsedThenable = null; +} + +export function resetThenableStateOnCompletion() { + suspendedThenables = null; } export function throwIfInfinitePingLoopDetected() { @@ -48,3 +61,24 @@ export function throwIfInfinitePingLoopDetected() { // the render phase so that it gets the component stack. } } + +export function trackSuspendedThenable( + thenable: Thenable, + index: number, +) { + if (suspendedThenables === null) { + suspendedThenables = []; + } + suspendedThenables[index] = thenable; + lastUsedThenable = thenable; +} + +export function getSuspendedThenable(index: number): Thenable | null { + if (suspendedThenables !== null) { + const thenable = suspendedThenables[index]; + if (thenable !== undefined) { + return thenable; + } + } + return null; +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index a8844d2c1a411..80efac3613e2d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -247,7 +247,8 @@ import { } from './ReactFiberAct.new'; import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new'; import { - resetWakeableState, + resetWakeableStateAfterEachAttempt, + resetThenableStateOnCompletion, trackSuspendedWakeable, suspendedWakeableWasPinged, attemptToPingSuspendedWakeable, @@ -1556,7 +1557,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { ); interruptedWork = interruptedWork.return; } - resetWakeableState(); + resetWakeableStateAfterEachAttempt(); + resetThenableStateOnCompletion(); } workInProgressRoot = root; const rootWorkInProgress = createWorkInProgress(root.current, null); @@ -1588,6 +1590,7 @@ function handleError(root, thrownValue): Wakeable | null { // Reset module-level state that was set during the render phase. resetContextDependencies(); resetHooksAfterThrow(); + resetWakeableStateAfterEachAttempt(); resetCurrentDebugFiberInDEV(); // TODO: I found and added this missing line while investigating a // separate issue. Write a regression test using string refs. @@ -1983,7 +1986,8 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void { // The wakeable wasn't pinged. Return to the normal work loop. This will // unwind the stack, and potentially result in showing a fallback. workInProgressIsSuspended = false; - resetWakeableState(); + resetWakeableStateAfterEachAttempt(); + resetThenableStateOnCompletion(); completeUnitOfWork(unitOfWork); return; } @@ -2009,7 +2013,8 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void { // used to track the fiber while it was suspended. Then return to the normal // work loop. workInProgressIsSuspended = false; - resetWakeableState(); + resetWakeableStateAfterEachAttempt(); + resetThenableStateOnCompletion(); resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; @@ -3143,6 +3148,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { // corresponding changes there. resetContextDependencies(); resetHooksAfterThrow(); + resetWakeableStateAfterEachAttempt(); // Don't reset current debug fiber, since we're about to work on the // same fiber again. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 14e378794bfbc..1e74aefd9b6d6 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -247,7 +247,8 @@ import { } from './ReactFiberAct.old'; import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old'; import { - resetWakeableState, + resetWakeableStateAfterEachAttempt, + resetThenableStateOnCompletion, trackSuspendedWakeable, suspendedWakeableWasPinged, attemptToPingSuspendedWakeable, @@ -1556,7 +1557,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { ); interruptedWork = interruptedWork.return; } - resetWakeableState(); + resetWakeableStateAfterEachAttempt(); + resetThenableStateOnCompletion(); } workInProgressRoot = root; const rootWorkInProgress = createWorkInProgress(root.current, null); @@ -1588,6 +1590,7 @@ function handleError(root, thrownValue): Wakeable | null { // Reset module-level state that was set during the render phase. resetContextDependencies(); resetHooksAfterThrow(); + resetWakeableStateAfterEachAttempt(); resetCurrentDebugFiberInDEV(); // TODO: I found and added this missing line while investigating a // separate issue. Write a regression test using string refs. @@ -1983,7 +1986,8 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void { // The wakeable wasn't pinged. Return to the normal work loop. This will // unwind the stack, and potentially result in showing a fallback. workInProgressIsSuspended = false; - resetWakeableState(); + resetWakeableStateAfterEachAttempt(); + resetThenableStateOnCompletion(); completeUnitOfWork(unitOfWork); return; } @@ -2009,7 +2013,8 @@ function resumeSuspendedUnitOfWork(unitOfWork: Fiber): void { // used to track the fiber while it was suspended. Then return to the normal // work loop. workInProgressIsSuspended = false; - resetWakeableState(); + resetWakeableStateAfterEachAttempt(); + resetThenableStateOnCompletion(); resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; @@ -3143,6 +3148,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { // corresponding changes there. resetContextDependencies(); resetHooksAfterThrow(); + resetWakeableStateAfterEachAttempt(); // Don't reset current debug fiber, since we're about to work on the // same fiber again. diff --git a/packages/react-reconciler/src/__tests__/ReactWakeable-test.js b/packages/react-reconciler/src/__tests__/ReactWakeable-test.js index 848962c696c0b..d00fc45bd686d 100644 --- a/packages/react-reconciler/src/__tests__/ReactWakeable-test.js +++ b/packages/react-reconciler/src/__tests__/ReactWakeable-test.js @@ -4,6 +4,7 @@ let React; let ReactNoop; let Scheduler; let act; +let use; let Suspense; let startTransition; @@ -15,6 +16,7 @@ describe('ReactWakeable', () => { ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); act = require('jest-react').act; + use = React.experimental_use; Suspense = React.Suspense; startTransition = React.startTransition; }); @@ -64,4 +66,73 @@ describe('ReactWakeable', () => { // Finished rendering without unwinding the stack. expect(Scheduler).toHaveYielded(['Async']); }); + + // @gate enableUseHook + test('basic use(promise)', async () => { + const promiseA = Promise.resolve('A'); + const promiseB = Promise.resolve('B'); + const promiseC = Promise.resolve('C'); + + function Async() { + const text = use(promiseA) + use(promiseB) + use(promiseC); + return ; + } + + function App() { + return ( + }> + + + ); + } + + const root = ReactNoop.createRoot(); + startTransition(() => { + root.render(); + }); + + // TODO: `act` should automatically yield to microtasks after something + // suspends, like Scheduler does. + expect(Scheduler).toFlushUntilNextPaint([]); + await null; + expect(Scheduler).toFlushUntilNextPaint([]); + await null; + expect(Scheduler).toFlushUntilNextPaint([]); + await null; + expect(Scheduler).toFlushUntilNextPaint(['ABC']); + }); + + // @gate enableUseHook + test("using a promise that's not cached between attempts", async () => { + function Async() { + const text = + use(Promise.resolve('A')) + + use(Promise.resolve('B')) + + use(Promise.resolve('C')); + return ; + } + + function App() { + return ( + }> + + + ); + } + + const root = ReactNoop.createRoot(); + startTransition(() => { + root.render(); + }); + + // TODO: `act` should automatically yield to microtasks after something + // suspends, like Scheduler does. + expect(Scheduler).toFlushUntilNextPaint([]); + await null; + expect(Scheduler).toFlushUntilNextPaint([]); + await null; + expect(Scheduler).toFlushUntilNextPaint([]); + await null; + expect(Scheduler).toFlushUntilNextPaint(['ABC']); + }); }); diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index ca6ea4cdef010..7dacd489e4b8a 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -174,18 +174,35 @@ export interface Wakeable { // The subset of a Promise that React APIs rely on. This resolves a value. // This doesn't require a return value neither from the handler nor the // then function. -export interface Thenable<+R> { - then( - onFulfill: (value: R) => void | Thenable | U, - onReject: (error: mixed) => void | Thenable | U, - ): void | Thenable; - - status?: 'pending' | 'fulfilled' | 'rejected'; - // `value` is type R when fulfilled, and mixed when rejected. - // TODO: There's probably a correct way to model this in newer versions of - // Flow; need to upgrade. - value?: any; +interface ThenableImpl { + then( + onFulfill: (value: T) => mixed, + onReject: (error: mixed) => mixed, + ): void | Wakeable; } +interface UntrackedThenable extends ThenableImpl { + status?: void; +} + +export interface PendingThenable extends ThenableImpl { + status: 'pending'; +} + +export interface FulfilledThenable extends ThenableImpl { + status: 'fulfilled'; + value: T; +} + +export interface RejectedThenable extends ThenableImpl { + status: 'rejected'; + reason: mixed; +} + +export type Thenable = + | UntrackedThenable + | PendingThenable + | FulfilledThenable + | RejectedThenable; export type OffscreenMode = | 'hidden' diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 010afa06e70f3..c5eebc34edefc 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -420,5 +420,6 @@ "432": "The render was aborted by the server without a reason.", "433": "useId can only be used while React is rendering", "434": "`dangerouslySetInnerHTML` does not make sense on .", - "435": "Unexpected Suspense handler tag (%s). This is a bug in React." + "435": "Unexpected Suspense handler tag (%s). This is a bug in React.", + "436": "An unsupported type was passed to use(): %s" }