Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify promise switch statements #25539

Merged
merged 3 commits into from
Oct 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 7 additions & 56 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import {
requestEventTime,
markSkippedUpdateLanes,
isInvalidExecutionContextForEventFunction,
getSuspendedThenableState,
} from './ReactFiberWorkLoop.new';

import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
Expand Down Expand Up @@ -134,8 +135,8 @@ import {
import {getTreeId} from './ReactFiberTreeContext.new';
import {now} from './Scheduler';
import {
prepareThenableState,
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberThenable.new';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
Expand Down Expand Up @@ -465,6 +466,9 @@ export function renderWithHooks<Props, SecondArg>(
: HooksDispatcherOnUpdate;
}

// If this is a replay, restore the thenable state from the previous attempt.
const prevThenableState = getSuspendedThenableState();
prepareThenableState(prevThenableState);
let children = Component(props, secondArg);

// Check if there was a render phase update
Expand Down Expand Up @@ -506,6 +510,7 @@ export function renderWithHooks<Props, SecondArg>(
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;

prepareThenableState(prevThenableState);
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
Expand Down Expand Up @@ -770,8 +775,6 @@ if (enableUseMemoCacheHook) {
};
}

function noop(): void {}

function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding]
Expand All @@ -782,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;

// TODO: Unify this switch statement with the one in trackUsedThenable.
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) {
if (thenable !== prevThenableAtIndex) {
// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
thenable.then(noop, noop);
}
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;
}
}
}
return trackUsedThenable(thenable, index);
} else if (
usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
Expand Down
63 changes: 7 additions & 56 deletions packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import {
requestEventTime,
markSkippedUpdateLanes,
isInvalidExecutionContextForEventFunction,
getSuspendedThenableState,
} from './ReactFiberWorkLoop.old';

import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
Expand Down Expand Up @@ -134,8 +135,8 @@ import {
import {getTreeId} from './ReactFiberTreeContext.old';
import {now} from './Scheduler';
import {
prepareThenableState,
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberThenable.old';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
Expand Down Expand Up @@ -465,6 +466,9 @@ export function renderWithHooks<Props, SecondArg>(
: HooksDispatcherOnUpdate;
}

// If this is a replay, restore the thenable state from the previous attempt.
const prevThenableState = getSuspendedThenableState();
prepareThenableState(prevThenableState);
let children = Component(props, secondArg);

// Check if there was a render phase update
Expand Down Expand Up @@ -506,6 +510,7 @@ export function renderWithHooks<Props, SecondArg>(
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;

prepareThenableState(prevThenableState);
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
Expand Down Expand Up @@ -770,8 +775,6 @@ if (enableUseMemoCacheHook) {
};
}

function noop(): void {}

function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding]
Expand All @@ -782,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;

// TODO: Unify this switch statement with the one in trackUsedThenable.
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) {
if (thenable !== prevThenableAtIndex) {
// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
thenable.then(noop, noop);
}
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;
}
}
}
return trackUsedThenable(thenable, index);
} else if (
usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
Expand Down
153 changes: 91 additions & 62 deletions packages/react-reconciler/src/ReactFiberThenable.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,75 @@ import type {
import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals;

let suspendedThenable: Thenable<any> | null = null;
let usedThenables: Array<Thenable<any> | void> | null = null;
export opaque type ThenableState = Array<Thenable<any>>;

export function isTrackingSuspendedThenable(): boolean {
return suspendedThenable !== null;
let thenableState: ThenableState | null = null;

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

export function prepareThenableState(prevThenableState: ThenableState | null) {
// This function is called before every function that might suspend
// with `use`. Right now, that's only Hooks, but in the future we'll use the
// same mechanism for unwrapping promises during reconciliation.
thenableState = prevThenableState;
}

export function suspendedThenableDidResolve(): boolean {
if (suspendedThenable !== null) {
const status = suspendedThenable.status;
export function getThenableStateAfterSuspending(): ThenableState | null {
// Called by the work loop so it can stash the thenable state. It will use
// the state to replay the component when the promise resolves.
if (
thenableState !== null &&
// If we only `use`-ed resolved promises, then there is no suspended state
// TODO: The only reason we do this is to distinguish between throwing a
// promise (old Suspense pattern) versus `use`-ing one. A better solution is
// for `use` to throw a special, opaque value instead of a promise.
!isThenableStateResolved(thenableState)
) {
const state = thenableState;
thenableState = null;
return state;
}
return null;
}

export function isThenableStateResolved(thenables: ThenableState): boolean {
const lastThenable = thenables[thenables.length - 1];
if (lastThenable !== undefined) {
const status = lastThenable.status;
return status === 'fulfilled' || status === 'rejected';
}
return false;
return true;
}

export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
function noop(): void {}

export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true;
}

if (usedThenables === null) {
usedThenables = [thenable];
if (thenableState === null) {
thenableState = [thenable];
} else {
usedThenables[index] = thenable;
}
const previous = thenableState[index];
if (previous === undefined) {
thenableState.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.

suspendedThenable = thenable;
// Avoid an unhandled rejection errors for the Promises that we'll
// intentionally ignore.
thenable.then(noop, noop);
thenable = previous;
}
}
}

// We use an expando to track the status and result of a thenable so that we
// can synchronously unwrap the value. Think of this as an extension of the
Expand All @@ -52,61 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
// If the thenable doesn't have a status, set it to "pending" and attach
// a listener that will update its status and result when it resolves.
switch (thenable.status) {
case 'fulfilled':
case 'rejected':
// A thenable that already resolved shouldn't have been thrown, so this is
// unexpected. Suggests a mistake in a userspace data library. Don't track
// this thenable, because if we keep trying it will likely infinite loop
// without ever resolving.
// TODO: Log a warning?
suspendedThenable = null;
break;
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
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".
break;
} else {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
}
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}

export function resetWakeableStateAfterEachAttempt() {
suspendedThenable = null;
}

export function resetThenableStateOnCompletion() {
usedThenables = null;
}

export function getPreviouslyUsedThenableAtIndex<T>(
index: number,
): Thenable<T> | null {
if (usedThenables !== null) {
const thenable = usedThenables[index];
if (thenable !== undefined) {
return thenable;
// 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;
}
}
return null;
}
Loading