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." }