From f284d9fafac3af79c6901b6b63305cbd161304cd Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 3 Nov 2022 19:25:43 -0400 Subject: [PATCH] Track ThenableState alongside other hooks Now that hook state is preserved while the work loop is suspended, we don't need to track the thenable state in the work loop. We can track it alongside the rest of the hook state. This is a nice simplification and also aligns better with how it works in Fizz and Flight. The promises will still be cleared when the component finishes rendering (either complete or unwind). In the future, we could stash the promises on the fiber and reuse them during an update. However, this would only work for `use` calls that occur before an prop/state/context is processed, because `use` calls can only be assumed to execute in the same order if no other props/state/context have changed. So it might not be worth doing until we have finer grained memoization. --- .../src/ReactFiberBeginWork.new.js | 3 - .../src/ReactFiberBeginWork.old.js | 3 - .../src/ReactFiberHooks.new.js | 24 ++-- .../src/ReactFiberHooks.old.js | 24 ++-- .../src/ReactFiberThenable.new.js | 49 +++------ .../src/ReactFiberThenable.old.js | 49 +++------ .../src/ReactFiberWorkLoop.new.js | 104 ++++++++---------- .../src/ReactFiberWorkLoop.old.js | 104 ++++++++---------- scripts/error-codes/codes.json | 3 +- 9 files changed, 142 insertions(+), 221 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 70a0c69542ad1..50dd170a6028f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -38,7 +38,6 @@ import type { import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new'; import type {RootState} from './ReactFiberRoot.new'; import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new'; -import type {ThenableState} from './ReactFiberThenable.new'; import checkPropTypes from 'shared/checkPropTypes'; import { @@ -1167,7 +1166,6 @@ export function replayFunctionComponent( workInProgress: Fiber, nextProps: any, Component: any, - prevThenableState: ThenableState, renderLanes: Lanes, ): Fiber | null { // This function is used to replay a component that previously suspended, @@ -1190,7 +1188,6 @@ export function replayFunctionComponent( Component, nextProps, context, - prevThenableState, ); const hasId = checkDidRenderIdHook(); if (enableSchedulingProfiler) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 1c22e2a7da834..1f22a6e18bcf5 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -38,7 +38,6 @@ import type { import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; import type {RootState} from './ReactFiberRoot.old'; import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old'; -import type {ThenableState} from './ReactFiberThenable.old'; import checkPropTypes from 'shared/checkPropTypes'; import { @@ -1167,7 +1166,6 @@ export function replayFunctionComponent( workInProgress: Fiber, nextProps: any, Component: any, - prevThenableState: ThenableState, renderLanes: Lanes, ): Fiber | null { // This function is used to replay a component that previously suspended, @@ -1190,7 +1188,6 @@ export function replayFunctionComponent( Component, nextProps, context, - prevThenableState, ); const hasId = checkDidRenderIdHook(); if (enableSchedulingProfiler) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 5ed7d34e1e784..3f97d0e8639c0 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -105,7 +105,6 @@ import { requestEventTime, markSkippedUpdateLanes, isInvalidExecutionContextForEventFunction, - getSuspendedThenableState, } from './ReactFiberWorkLoop.new'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -141,9 +140,9 @@ import { import {getTreeId} from './ReactFiberTreeContext.new'; import {now} from './Scheduler'; import { - prepareThenableState, trackUsedThenable, checkIfUseWrappedInTryCatch, + createThenableState, } from './ReactFiberThenable.new'; import type {ThenableState} from './ReactFiberThenable.new'; @@ -247,6 +246,7 @@ let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false; let localIdCounter: number = 0; // Counts number of `use`-d thenables let thenableIndexCounter: number = 0; +let thenableState: ThenableState | null = null; // 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 @@ -449,6 +449,7 @@ export function renderWithHooks( // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; // thenableIndexCounter = 0; + // thenableState = null; // 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. @@ -477,10 +478,6 @@ export function renderWithHooks( : HooksDispatcherOnUpdate; } - // If this is a replay, restore the thenable state from the previous attempt. - const prevThenableState = getSuspendedThenableState(); - prepareThenableState(prevThenableState); - // In Strict Mode, during development, user functions are double invoked to // help detect side effects. The logic for how this is implemented for in // hook components is a bit complex so let's break it down. @@ -525,7 +522,6 @@ export function renderWithHooks( Component, props, secondArg, - prevThenableState, ); } @@ -538,7 +534,6 @@ export function renderWithHooks( Component, props, secondArg, - prevThenableState, ); } finally { setIsStrictModeForDevtools(false); @@ -600,7 +595,9 @@ function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) { didScheduleRenderPhaseUpdate = false; // This is reset by checkDidRenderIdHook // localIdCounter = 0; + thenableIndexCounter = 0; + thenableState = null; if (didRenderTooFewHooks) { throw new Error( @@ -652,7 +649,6 @@ export function replaySuspendedComponentWithHooks( Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, - prevThenableState: ThenableState | null, ): any { // This function is used to replay a component that previously suspended, // after its data resolves. @@ -676,7 +672,6 @@ export function replaySuspendedComponentWithHooks( Component, props, secondArg, - prevThenableState, ); finishRenderingHooks(current, workInProgress); return children; @@ -687,7 +682,6 @@ function renderWithHooksAgain( Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, - prevThenableState: ThenableState | null, ) { // This is used to perform another render pass. It's used when setState is // called during render, and for double invoking components in Strict Mode @@ -735,7 +729,6 @@ function renderWithHooksAgain( ? HooksDispatcherOnRerenderInDEV : HooksDispatcherOnRerender; - prepareThenableState(prevThenableState); children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); return children; @@ -821,6 +814,7 @@ export function resetHooksOnUnwind(): void { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; thenableIndexCounter = 0; + thenableState = null; } function mountWorkInProgressHook(): Hook { @@ -954,7 +948,11 @@ function use(usable: Usable): T { // Track the position of the thenable within this fiber. const index = thenableIndexCounter; thenableIndexCounter += 1; - return trackUsedThenable(thenable, index); + + if (thenableState === null) { + thenableState = createThenableState(); + } + return trackUsedThenable(thenableState, thenable, index); } else if ( usable.$$typeof === REACT_CONTEXT_TYPE || usable.$$typeof === REACT_SERVER_CONTEXT_TYPE diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index fb98e9b7b58db..6c272d6b0613d 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -105,7 +105,6 @@ import { requestEventTime, markSkippedUpdateLanes, isInvalidExecutionContextForEventFunction, - getSuspendedThenableState, } from './ReactFiberWorkLoop.old'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -141,9 +140,9 @@ import { import {getTreeId} from './ReactFiberTreeContext.old'; import {now} from './Scheduler'; import { - prepareThenableState, trackUsedThenable, checkIfUseWrappedInTryCatch, + createThenableState, } from './ReactFiberThenable.old'; import type {ThenableState} from './ReactFiberThenable.old'; @@ -247,6 +246,7 @@ let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false; let localIdCounter: number = 0; // Counts number of `use`-d thenables let thenableIndexCounter: number = 0; +let thenableState: ThenableState | null = null; // 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 @@ -449,6 +449,7 @@ export function renderWithHooks( // didScheduleRenderPhaseUpdate = false; // localIdCounter = 0; // thenableIndexCounter = 0; + // thenableState = null; // 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. @@ -477,10 +478,6 @@ export function renderWithHooks( : HooksDispatcherOnUpdate; } - // If this is a replay, restore the thenable state from the previous attempt. - const prevThenableState = getSuspendedThenableState(); - prepareThenableState(prevThenableState); - // In Strict Mode, during development, user functions are double invoked to // help detect side effects. The logic for how this is implemented for in // hook components is a bit complex so let's break it down. @@ -525,7 +522,6 @@ export function renderWithHooks( Component, props, secondArg, - prevThenableState, ); } @@ -538,7 +534,6 @@ export function renderWithHooks( Component, props, secondArg, - prevThenableState, ); } finally { setIsStrictModeForDevtools(false); @@ -600,7 +595,9 @@ function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) { didScheduleRenderPhaseUpdate = false; // This is reset by checkDidRenderIdHook // localIdCounter = 0; + thenableIndexCounter = 0; + thenableState = null; if (didRenderTooFewHooks) { throw new Error( @@ -652,7 +649,6 @@ export function replaySuspendedComponentWithHooks( Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, - prevThenableState: ThenableState | null, ): any { // This function is used to replay a component that previously suspended, // after its data resolves. @@ -676,7 +672,6 @@ export function replaySuspendedComponentWithHooks( Component, props, secondArg, - prevThenableState, ); finishRenderingHooks(current, workInProgress); return children; @@ -687,7 +682,6 @@ function renderWithHooksAgain( Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, - prevThenableState: ThenableState | null, ) { // This is used to perform another render pass. It's used when setState is // called during render, and for double invoking components in Strict Mode @@ -735,7 +729,6 @@ function renderWithHooksAgain( ? HooksDispatcherOnRerenderInDEV : HooksDispatcherOnRerender; - prepareThenableState(prevThenableState); children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass); return children; @@ -821,6 +814,7 @@ export function resetHooksOnUnwind(): void { didScheduleRenderPhaseUpdateDuringThisPass = false; localIdCounter = 0; thenableIndexCounter = 0; + thenableState = null; } function mountWorkInProgressHook(): Hook { @@ -954,7 +948,11 @@ function use(usable: Usable): T { // Track the position of the thenable within this fiber. const index = thenableIndexCounter; thenableIndexCounter += 1; - return trackUsedThenable(thenable, index); + + if (thenableState === null) { + thenableState = createThenableState(); + } + return trackUsedThenable(thenableState, thenable, index); } else if ( usable.$$typeof === REACT_CONTEXT_TYPE || usable.$$typeof === REACT_SERVER_CONTEXT_TYPE diff --git a/packages/react-reconciler/src/ReactFiberThenable.new.js b/packages/react-reconciler/src/ReactFiberThenable.new.js index c13bf7ebc54df..53d1c7176d588 100644 --- a/packages/react-reconciler/src/ReactFiberThenable.new.js +++ b/packages/react-reconciler/src/ReactFiberThenable.new.js @@ -31,29 +31,12 @@ export const SuspenseException: mixed = new Error( "call the promise's `.catch` method and pass the result to `use`", ); -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 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. - const state = thenableState; - thenableState = null; - return state; -} - export function isThenableResolved(thenable: Thenable): boolean { const status = thenable.status; return status === 'fulfilled' || status === 'rejected'; @@ -61,27 +44,27 @@ export function isThenableResolved(thenable: Thenable): boolean { function noop(): void {} -export function trackUsedThenable(thenable: Thenable, index: number): T { +export function trackUsedThenable( + thenableState: ThenableState, + thenable: Thenable, + index: number, +): T { if (__DEV__ && ReactCurrentActQueue.current !== null) { ReactCurrentActQueue.didUsePromise = true; } - if (thenableState === null) { - thenableState = [thenable]; + const previous = thenableState[index]; + if (previous === undefined) { + thenableState.push(thenable); } else { - 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. - - // Avoid an unhandled rejection errors for the Promises that we'll - // intentionally ignore. - thenable.then(noop, noop); - thenable = previous; - } + if (previous !== thenable) { + // Reuse the previous thenable, and drop the new one. We can assume + // they represent the same value, because components are idempotent. + + // Avoid an unhandled rejection errors for the Promises that we'll + // intentionally ignore. + thenable.then(noop, noop); + thenable = previous; } } diff --git a/packages/react-reconciler/src/ReactFiberThenable.old.js b/packages/react-reconciler/src/ReactFiberThenable.old.js index c13bf7ebc54df..53d1c7176d588 100644 --- a/packages/react-reconciler/src/ReactFiberThenable.old.js +++ b/packages/react-reconciler/src/ReactFiberThenable.old.js @@ -31,29 +31,12 @@ export const SuspenseException: mixed = new Error( "call the promise's `.catch` method and pass the result to `use`", ); -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 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. - const state = thenableState; - thenableState = null; - return state; -} - export function isThenableResolved(thenable: Thenable): boolean { const status = thenable.status; return status === 'fulfilled' || status === 'rejected'; @@ -61,27 +44,27 @@ export function isThenableResolved(thenable: Thenable): boolean { function noop(): void {} -export function trackUsedThenable(thenable: Thenable, index: number): T { +export function trackUsedThenable( + thenableState: ThenableState, + thenable: Thenable, + index: number, +): T { if (__DEV__ && ReactCurrentActQueue.current !== null) { ReactCurrentActQueue.didUsePromise = true; } - if (thenableState === null) { - thenableState = [thenable]; + const previous = thenableState[index]; + if (previous === undefined) { + thenableState.push(thenable); } else { - 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. - - // Avoid an unhandled rejection errors for the Promises that we'll - // intentionally ignore. - thenable.then(noop, noop); - thenable = previous; - } + if (previous !== thenable) { + // Reuse the previous thenable, and drop the new one. We can assume + // they represent the same value, because components are idempotent. + + // Avoid an unhandled rejection errors for the Promises that we'll + // intentionally ignore. + thenable.then(noop, noop); + thenable = previous; } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index bab5075d200ae..b8135becc4360 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -25,7 +25,6 @@ import type { TransitionAbort, } from './ReactFiberTracingMarkerComponent.new'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; -import type {ThenableState} from './ReactFiberThenable.new'; import { warnAboutDeprecatedLifecycles, @@ -275,7 +274,6 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new import { SuspenseException, getSuspendedThenable, - getThenableStateAfterSuspending, isThenableResolved, } from './ReactFiberThenable.new'; import {schedulePostPaintCallback} from './ReactPostPaintCallback'; @@ -322,13 +320,14 @@ let workInProgress: Fiber | null = null; // The lanes we're rendering let workInProgressRootRenderLanes: Lanes = NoLanes; -opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5; +opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5 | 6; const NotSuspended: SuspendedReason = 0; const SuspendedOnError: SuspendedReason = 1; const SuspendedOnData: SuspendedReason = 2; const SuspendedOnImmediate: SuspendedReason = 3; -const SuspendedAndReadyToUnwind: SuspendedReason = 4; -const SuspendedOnHydration: SuspendedReason = 5; +const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4; +const SuspendedAndReadyToUnwind: SuspendedReason = 5; +const SuspendedOnHydration: SuspendedReason = 6; // When this is true, the work-in-progress fiber just suspended (or errored) and // we've yet to unwind the stack. In some cases, we may yield to the main thread @@ -336,7 +335,6 @@ const SuspendedOnHydration: SuspendedReason = 5; // immediately instead of unwinding the stack. let workInProgressSuspendedReason: SuspendedReason = NotSuspended; let workInProgressThrownValue: mixed = null; -let workInProgressSuspendedThenableState: ThenableState | null = null; // Whether a ping listener was attached during this render. This is slightly // different that whether something suspended, because we don't add multiple @@ -1749,7 +1747,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressRootRenderLanes = renderLanes = lanes; workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - workInProgressSuspendedThenableState = null; workInProgressRootDidAttachPingListener = false; workInProgressRootExitStatus = RootInProgress; workInProgressRootFatalError = null; @@ -1802,7 +1799,6 @@ function handleThrow(root, thrownValue): void { // API for suspending. This implementation detail can change later, once we // deprecate the old API in favor of `use`. thrownValue = getSuspendedThenable(); - workInProgressSuspendedThenableState = getThenableStateAfterSuspending(); workInProgressSuspendedReason = shouldAttemptToSuspendUntilDataResolves() ? SuspendedOnData : SuspendedOnImmediate; @@ -1816,13 +1812,9 @@ function handleThrow(root, thrownValue): void { // // We could name this something more general but as of now it's the only // case where we think this should happen. - workInProgressSuspendedThenableState = null; workInProgressSuspendedReason = SuspendedOnHydration; } else { - // This is a regular error. If something earlier in the component already - // suspended, we must clear the thenable state to unblock the work loop. - workInProgressSuspendedThenableState = null; - + // This is a regular error. const isWakeable = thrownValue !== null && typeof thrownValue === 'object' && @@ -1832,7 +1824,7 @@ function handleThrow(root, thrownValue): void { workInProgressSuspendedReason = isWakeable ? // A wakeable object was thrown by a legacy Suspense implementation. // This has slightly different behavior than suspending with `use`. - SuspendedAndReadyToUnwind + SuspendedOnDeprecatedThrowPromise : // This is a regular error. If something earlier in the component already // suspended, we must clear the thenable state to unblock the work loop. SuspendedOnError; @@ -2205,17 +2197,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } case SuspendedOnData: { const thenable: Thenable = (thrownValue: any); - if (workInProgressSuspendedThenableState !== null) { - const thenableState = workInProgressSuspendedThenableState; - if (isThenableResolved(thenable)) { - // The data resolved. Try rendering the component again. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - replaySuspendedUnitOfWork(unitOfWork, thenable, thenableState); - break; - } + if (isThenableResolved(thenable)) { + // The data resolved. Try rendering the component again. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + replaySuspendedUnitOfWork(unitOfWork); + break; } - // The work loop is suspended on data. We should wait for it to // resolve before continuing to render. const onResolution = () => { @@ -2231,6 +2219,31 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { workInProgressSuspendedReason = SuspendedAndReadyToUnwind; break outer; } + case SuspendedAndReadyToUnwind: { + const thenable: Thenable = (thrownValue: any); + if (isThenableResolved(thenable)) { + // The data resolved. Try rendering the component again. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + replaySuspendedUnitOfWork(unitOfWork); + } else { + // Otherwise, unwind then continue with the normal work loop. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + } + break; + } + case SuspendedOnDeprecatedThrowPromise: { + // Suspended by an old implementation that uses the `throw promise` + // pattern. The newer replaying behavior can cause subtle issues + // like infinite ping loops. So we maintain the old behavior and + // always unwind. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + break; + } case SuspendedOnHydration: { // Selective hydration. An update flowed into a dehydrated tree. // Interrupt the current render so the work loop can switch to the @@ -2240,27 +2253,9 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { break outer; } default: { - if (workInProgressSuspendedThenableState !== null) { - const thenableState = workInProgressSuspendedThenableState; - const thenable: Thenable = (thrownValue: any); - if (isThenableResolved(thenable)) { - // The data resolved. Try rendering the component again. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - replaySuspendedUnitOfWork( - unitOfWork, - thrownValue, - thenableState, - ); - break; - } - } - - // Otherwise, unwind then continue with the normal work loop. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); - break; + throw new Error( + 'Unexpected SuspendedReason. This is a bug in React.', + ); } } } @@ -2344,11 +2339,7 @@ function performUnitOfWork(unitOfWork: Fiber): void { ReactCurrentOwner.current = null; } -function replaySuspendedUnitOfWork( - unitOfWork: Fiber, - thrownValue: mixed, - thenableState: ThenableState, -): void { +function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { // This is a fork of performUnitOfWork specifcally for replaying a fiber that // just suspended. // @@ -2387,7 +2378,6 @@ function replaySuspendedUnitOfWork( unitOfWork, resolvedProps, Component, - thenableState, workInProgressRootRenderLanes, ); break; @@ -2400,7 +2390,6 @@ function replaySuspendedUnitOfWork( unitOfWork, nextProps, Component, - thenableState, workInProgressRootRenderLanes, ); break; @@ -2427,10 +2416,8 @@ function replaySuspendedUnitOfWork( stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } - // The begin phase finished successfully without suspending. Reset the state - // used to track the fiber while it was suspended. Then return to the normal - // work loop. - workInProgressSuspendedThenableState = null; + // The begin phase finished successfully without suspending. Return to the + // normal work loop. resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; @@ -2450,7 +2437,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { // // Return to the normal work loop. This will unwind the stack, and potentially // result in showing a fallback. - workInProgressSuspendedThenableState = null; resetSuspendedWorkLoopOnUnwind(); const returnFiber = unitOfWork.return; @@ -2494,10 +2480,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { completeUnitOfWork(unitOfWork); } -export function getSuspendedThenableState(): ThenableState | null { - return workInProgressSuspendedThenableState; -} - function completeUnitOfWork(unitOfWork: Fiber): void { // Attempt to complete the current unit of work, then move to the next // sibling. If there are no more siblings, return to the parent fiber. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 1f5b587e2af7f..3b571b1ceb3d7 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -25,7 +25,6 @@ import type { TransitionAbort, } from './ReactFiberTracingMarkerComponent.old'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; -import type {ThenableState} from './ReactFiberThenable.old'; import { warnAboutDeprecatedLifecycles, @@ -275,7 +274,6 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old import { SuspenseException, getSuspendedThenable, - getThenableStateAfterSuspending, isThenableResolved, } from './ReactFiberThenable.old'; import {schedulePostPaintCallback} from './ReactPostPaintCallback'; @@ -322,13 +320,14 @@ let workInProgress: Fiber | null = null; // The lanes we're rendering let workInProgressRootRenderLanes: Lanes = NoLanes; -opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5; +opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5 | 6; const NotSuspended: SuspendedReason = 0; const SuspendedOnError: SuspendedReason = 1; const SuspendedOnData: SuspendedReason = 2; const SuspendedOnImmediate: SuspendedReason = 3; -const SuspendedAndReadyToUnwind: SuspendedReason = 4; -const SuspendedOnHydration: SuspendedReason = 5; +const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4; +const SuspendedAndReadyToUnwind: SuspendedReason = 5; +const SuspendedOnHydration: SuspendedReason = 6; // When this is true, the work-in-progress fiber just suspended (or errored) and // we've yet to unwind the stack. In some cases, we may yield to the main thread @@ -336,7 +335,6 @@ const SuspendedOnHydration: SuspendedReason = 5; // immediately instead of unwinding the stack. let workInProgressSuspendedReason: SuspendedReason = NotSuspended; let workInProgressThrownValue: mixed = null; -let workInProgressSuspendedThenableState: ThenableState | null = null; // Whether a ping listener was attached during this render. This is slightly // different that whether something suspended, because we don't add multiple @@ -1749,7 +1747,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressRootRenderLanes = renderLanes = lanes; workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - workInProgressSuspendedThenableState = null; workInProgressRootDidAttachPingListener = false; workInProgressRootExitStatus = RootInProgress; workInProgressRootFatalError = null; @@ -1802,7 +1799,6 @@ function handleThrow(root, thrownValue): void { // API for suspending. This implementation detail can change later, once we // deprecate the old API in favor of `use`. thrownValue = getSuspendedThenable(); - workInProgressSuspendedThenableState = getThenableStateAfterSuspending(); workInProgressSuspendedReason = shouldAttemptToSuspendUntilDataResolves() ? SuspendedOnData : SuspendedOnImmediate; @@ -1816,13 +1812,9 @@ function handleThrow(root, thrownValue): void { // // We could name this something more general but as of now it's the only // case where we think this should happen. - workInProgressSuspendedThenableState = null; workInProgressSuspendedReason = SuspendedOnHydration; } else { - // This is a regular error. If something earlier in the component already - // suspended, we must clear the thenable state to unblock the work loop. - workInProgressSuspendedThenableState = null; - + // This is a regular error. const isWakeable = thrownValue !== null && typeof thrownValue === 'object' && @@ -1832,7 +1824,7 @@ function handleThrow(root, thrownValue): void { workInProgressSuspendedReason = isWakeable ? // A wakeable object was thrown by a legacy Suspense implementation. // This has slightly different behavior than suspending with `use`. - SuspendedAndReadyToUnwind + SuspendedOnDeprecatedThrowPromise : // This is a regular error. If something earlier in the component already // suspended, we must clear the thenable state to unblock the work loop. SuspendedOnError; @@ -2205,17 +2197,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } case SuspendedOnData: { const thenable: Thenable = (thrownValue: any); - if (workInProgressSuspendedThenableState !== null) { - const thenableState = workInProgressSuspendedThenableState; - if (isThenableResolved(thenable)) { - // The data resolved. Try rendering the component again. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - replaySuspendedUnitOfWork(unitOfWork, thenable, thenableState); - break; - } + if (isThenableResolved(thenable)) { + // The data resolved. Try rendering the component again. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + replaySuspendedUnitOfWork(unitOfWork); + break; } - // The work loop is suspended on data. We should wait for it to // resolve before continuing to render. const onResolution = () => { @@ -2231,6 +2219,31 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { workInProgressSuspendedReason = SuspendedAndReadyToUnwind; break outer; } + case SuspendedAndReadyToUnwind: { + const thenable: Thenable = (thrownValue: any); + if (isThenableResolved(thenable)) { + // The data resolved. Try rendering the component again. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + replaySuspendedUnitOfWork(unitOfWork); + } else { + // Otherwise, unwind then continue with the normal work loop. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + } + break; + } + case SuspendedOnDeprecatedThrowPromise: { + // Suspended by an old implementation that uses the `throw promise` + // pattern. The newer replaying behavior can cause subtle issues + // like infinite ping loops. So we maintain the old behavior and + // always unwind. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + break; + } case SuspendedOnHydration: { // Selective hydration. An update flowed into a dehydrated tree. // Interrupt the current render so the work loop can switch to the @@ -2240,27 +2253,9 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { break outer; } default: { - if (workInProgressSuspendedThenableState !== null) { - const thenableState = workInProgressSuspendedThenableState; - const thenable: Thenable = (thrownValue: any); - if (isThenableResolved(thenable)) { - // The data resolved. Try rendering the component again. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - replaySuspendedUnitOfWork( - unitOfWork, - thrownValue, - thenableState, - ); - break; - } - } - - // Otherwise, unwind then continue with the normal work loop. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); - break; + throw new Error( + 'Unexpected SuspendedReason. This is a bug in React.', + ); } } } @@ -2344,11 +2339,7 @@ function performUnitOfWork(unitOfWork: Fiber): void { ReactCurrentOwner.current = null; } -function replaySuspendedUnitOfWork( - unitOfWork: Fiber, - thrownValue: mixed, - thenableState: ThenableState, -): void { +function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { // This is a fork of performUnitOfWork specifcally for replaying a fiber that // just suspended. // @@ -2387,7 +2378,6 @@ function replaySuspendedUnitOfWork( unitOfWork, resolvedProps, Component, - thenableState, workInProgressRootRenderLanes, ); break; @@ -2400,7 +2390,6 @@ function replaySuspendedUnitOfWork( unitOfWork, nextProps, Component, - thenableState, workInProgressRootRenderLanes, ); break; @@ -2427,10 +2416,8 @@ function replaySuspendedUnitOfWork( stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } - // The begin phase finished successfully without suspending. Reset the state - // used to track the fiber while it was suspended. Then return to the normal - // work loop. - workInProgressSuspendedThenableState = null; + // The begin phase finished successfully without suspending. Return to the + // normal work loop. resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; @@ -2450,7 +2437,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { // // Return to the normal work loop. This will unwind the stack, and potentially // result in showing a fallback. - workInProgressSuspendedThenableState = null; resetSuspendedWorkLoopOnUnwind(); const returnFiber = unitOfWork.return; @@ -2494,10 +2480,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { completeUnitOfWork(unitOfWork); } -export function getSuspendedThenableState(): ThenableState | null { - return workInProgressSuspendedThenableState; -} - function completeUnitOfWork(unitOfWork: Fiber): void { // Attempt to complete the current unit of work, then move to the next // sibling. If there are no more siblings, return to the parent fiber. diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 03ccbc3e76a25..b4ea290152e4d 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -446,5 +446,6 @@ "458": "Currently React only supports one RSC renderer at a time.", "459": "Expected a suspended thenable. This is a bug in React. Please file an issue.", "460": "Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render. You must either rethrow it immediately, or move the `use` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary, or call the promise's `.catch` method and pass the result to `use`", - "461": "This is not a real error. It's an implementation detail of React's selective hydration feature. If this leaks into userspace, it's a bug in React. Please file an issue." + "461": "This is not a real error. It's an implementation detail of React's selective hydration feature. If this leaks into userspace, it's a bug in React. Please file an issue.", + "462": "Unexpected SuspendedReason. This is a bug in React." }