@samitouri / QOS-React / commits / f284d9fafa

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.

Andrew Clark committed Nov 3, 2022 at 19:25 UTC f284d9fafac3af79c6901b6b63305cbd161304cd
9 files changed +142 -221
packages/react-reconciler/src/ReactFiberBeginWork.new.js
-3
@@ -38,7 +38,6 @@ import type {
38 import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
39 import type {RootState} from './ReactFiberRoot.new';
40 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
41 -import type {ThenableState} from './ReactFiberThenable.new';
41
42 import checkPropTypes from 'shared/checkPropTypes';
43 import {
@@ -1167,7 +1166,6 @@ export function replayFunctionComponent(
1166 workInProgress: Fiber,
1167 nextProps: any,
1168 Component: any,
1170 - prevThenableState: ThenableState,
1169 renderLanes: Lanes,
1170 ): Fiber | null {
1171 // This function is used to replay a component that previously suspended,
@@ -1190,7 +1188,6 @@ export function replayFunctionComponent(
1188 Component,
1189 nextProps,
1190 context,
1193 - prevThenableState,
1191 );
1192 const hasId = checkDidRenderIdHook();
1193 if (enableSchedulingProfiler) {
packages/react-reconciler/src/ReactFiberBeginWork.old.js
-3
@@ -38,7 +38,6 @@ import type {
38 import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
39 import type {RootState} from './ReactFiberRoot.old';
40 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
41 -import type {ThenableState} from './ReactFiberThenable.old';
41
42 import checkPropTypes from 'shared/checkPropTypes';
43 import {
@@ -1167,7 +1166,6 @@ export function replayFunctionComponent(
1166 workInProgress: Fiber,
1167 nextProps: any,
1168 Component: any,
1170 - prevThenableState: ThenableState,
1169 renderLanes: Lanes,
1170 ): Fiber | null {
1171 // This function is used to replay a component that previously suspended,
@@ -1190,7 +1188,6 @@ export function replayFunctionComponent(
1188 Component,
1189 nextProps,
1190 context,
1193 - prevThenableState,
1191 );
1192 const hasId = checkDidRenderIdHook();
1193 if (enableSchedulingProfiler) {
packages/react-reconciler/src/ReactFiberHooks.new.js
+11 -13
@@ -105,7 +105,6 @@ import {
105 requestEventTime,
106 markSkippedUpdateLanes,
107 isInvalidExecutionContextForEventFunction,
108 - getSuspendedThenableState,
108 } from './ReactFiberWorkLoop.new';
109
110 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -141,9 +140,9 @@ import {
140 import {getTreeId} from './ReactFiberTreeContext.new';
141 import {now} from './Scheduler';
142 import {
144 - prepareThenableState,
143 trackUsedThenable,
144 checkIfUseWrappedInTryCatch,
145 + createThenableState,
146 } from './ReactFiberThenable.new';
147 import type {ThenableState} from './ReactFiberThenable.new';
148
@@ -247,6 +246,7 @@ let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
246 let localIdCounter: number = 0;
247 // Counts number of `use`-d thenables
248 let thenableIndexCounter: number = 0;
249 +let thenableState: ThenableState | null = null;
250
251 // Used for ids that are generated completely client-side (i.e. not during
252 // hydration). This counter is global, so client ids are not stable across
@@ -449,6 +449,7 @@ export function renderWithHooks<Props, SecondArg>(
449 // didScheduleRenderPhaseUpdate = false;
450 // localIdCounter = 0;
451 // thenableIndexCounter = 0;
452 + // thenableState = null;
453
454 // TODO Warn if no hooks are used at all during mount, then some are used during update.
455 // Currently we will identify the update render as a mount because memoizedState === null.
@@ -477,10 +478,6 @@ export function renderWithHooks<Props, SecondArg>(
478 : HooksDispatcherOnUpdate;
479 }
480
480 - // If this is a replay, restore the thenable state from the previous attempt.
481 - const prevThenableState = getSuspendedThenableState();
482 - prepareThenableState(prevThenableState);
483 -
481 // In Strict Mode, during development, user functions are double invoked to
482 // help detect side effects. The logic for how this is implemented for in
483 // hook components is a bit complex so let's break it down.
@@ -525,7 +522,6 @@ export function renderWithHooks<Props, SecondArg>(
522 Component,
523 props,
524 secondArg,
528 - prevThenableState,
525 );
526 }
527
@@ -538,7 +534,6 @@ export function renderWithHooks<Props, SecondArg>(
534 Component,
535 props,
536 secondArg,
541 - prevThenableState,
537 );
538 } finally {
539 setIsStrictModeForDevtools(false);
@@ -600,7 +595,9 @@ function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
595 didScheduleRenderPhaseUpdate = false;
596 // This is reset by checkDidRenderIdHook
597 // localIdCounter = 0;
598 +
599 thenableIndexCounter = 0;
600 + thenableState = null;
601
602 if (didRenderTooFewHooks) {
603 throw new Error(
@@ -652,7 +649,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
649 Component: (p: Props, arg: SecondArg) => any,
650 props: Props,
651 secondArg: SecondArg,
655 - prevThenableState: ThenableState | null,
652 ): any {
653 // This function is used to replay a component that previously suspended,
654 // after its data resolves.
@@ -676,7 +672,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
672 Component,
673 props,
674 secondArg,
679 - prevThenableState,
675 );
676 finishRenderingHooks(current, workInProgress);
677 return children;
@@ -687,7 +682,6 @@ function renderWithHooksAgain<Props, SecondArg>(
682 Component: (p: Props, arg: SecondArg) => any,
683 props: Props,
684 secondArg: SecondArg,
690 - prevThenableState: ThenableState | null,
685 ) {
686 // This is used to perform another render pass. It's used when setState is
687 // called during render, and for double invoking components in Strict Mode
@@ -735,7 +729,6 @@ function renderWithHooksAgain<Props, SecondArg>(
729 ? HooksDispatcherOnRerenderInDEV
730 : HooksDispatcherOnRerender;
731
738 - prepareThenableState(prevThenableState);
732 children = Component(props, secondArg);
733 } while (didScheduleRenderPhaseUpdateDuringThisPass);
734 return children;
@@ -821,6 +814,7 @@ export function resetHooksOnUnwind(): void {
814 didScheduleRenderPhaseUpdateDuringThisPass = false;
815 localIdCounter = 0;
816 thenableIndexCounter = 0;
817 + thenableState = null;
818 }
819
820 function mountWorkInProgressHook(): Hook {
@@ -954,7 +948,11 @@ function use<T>(usable: Usable<T>): T {
948 // Track the position of the thenable within this fiber.
949 const index = thenableIndexCounter;
950 thenableIndexCounter += 1;
957 - return trackUsedThenable(thenable, index);
951 +
952 + if (thenableState === null) {
953 + thenableState = createThenableState();
954 + }
955 + return trackUsedThenable(thenableState, thenable, index);
956 } else if (
957 usable.$$typeof === REACT_CONTEXT_TYPE ||
958 usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-reconciler/src/ReactFiberHooks.old.js
+11 -13
@@ -105,7 +105,6 @@ import {
105 requestEventTime,
106 markSkippedUpdateLanes,
107 isInvalidExecutionContextForEventFunction,
108 - getSuspendedThenableState,
108 } from './ReactFiberWorkLoop.old';
109
110 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -141,9 +140,9 @@ import {
140 import {getTreeId} from './ReactFiberTreeContext.old';
141 import {now} from './Scheduler';
142 import {
144 - prepareThenableState,
143 trackUsedThenable,
144 checkIfUseWrappedInTryCatch,
145 + createThenableState,
146 } from './ReactFiberThenable.old';
147 import type {ThenableState} from './ReactFiberThenable.old';
148
@@ -247,6 +246,7 @@ let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
246 let localIdCounter: number = 0;
247 // Counts number of `use`-d thenables
248 let thenableIndexCounter: number = 0;
249 +let thenableState: ThenableState | null = null;
250
251 // Used for ids that are generated completely client-side (i.e. not during
252 // hydration). This counter is global, so client ids are not stable across
@@ -449,6 +449,7 @@ export function renderWithHooks<Props, SecondArg>(
449 // didScheduleRenderPhaseUpdate = false;
450 // localIdCounter = 0;
451 // thenableIndexCounter = 0;
452 + // thenableState = null;
453
454 // TODO Warn if no hooks are used at all during mount, then some are used during update.
455 // Currently we will identify the update render as a mount because memoizedState === null.
@@ -477,10 +478,6 @@ export function renderWithHooks<Props, SecondArg>(
478 : HooksDispatcherOnUpdate;
479 }
480
480 - // If this is a replay, restore the thenable state from the previous attempt.
481 - const prevThenableState = getSuspendedThenableState();
482 - prepareThenableState(prevThenableState);
483 -
481 // In Strict Mode, during development, user functions are double invoked to
482 // help detect side effects. The logic for how this is implemented for in
483 // hook components is a bit complex so let's break it down.
@@ -525,7 +522,6 @@ export function renderWithHooks<Props, SecondArg>(
522 Component,
523 props,
524 secondArg,
528 - prevThenableState,
525 );
526 }
527
@@ -538,7 +534,6 @@ export function renderWithHooks<Props, SecondArg>(
534 Component,
535 props,
536 secondArg,
541 - prevThenableState,
537 );
538 } finally {
539 setIsStrictModeForDevtools(false);
@@ -600,7 +595,9 @@ function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
595 didScheduleRenderPhaseUpdate = false;
596 // This is reset by checkDidRenderIdHook
597 // localIdCounter = 0;
598 +
599 thenableIndexCounter = 0;
600 + thenableState = null;
601
602 if (didRenderTooFewHooks) {
603 throw new Error(
@@ -652,7 +649,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
649 Component: (p: Props, arg: SecondArg) => any,
650 props: Props,
651 secondArg: SecondArg,
655 - prevThenableState: ThenableState | null,
652 ): any {
653 // This function is used to replay a component that previously suspended,
654 // after its data resolves.
@@ -676,7 +672,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
672 Component,
673 props,
674 secondArg,
679 - prevThenableState,
675 );
676 finishRenderingHooks(current, workInProgress);
677 return children;
@@ -687,7 +682,6 @@ function renderWithHooksAgain<Props, SecondArg>(
682 Component: (p: Props, arg: SecondArg) => any,
683 props: Props,
684 secondArg: SecondArg,
690 - prevThenableState: ThenableState | null,
685 ) {
686 // This is used to perform another render pass. It's used when setState is
687 // called during render, and for double invoking components in Strict Mode
@@ -735,7 +729,6 @@ function renderWithHooksAgain<Props, SecondArg>(
729 ? HooksDispatcherOnRerenderInDEV
730 : HooksDispatcherOnRerender;
731
738 - prepareThenableState(prevThenableState);
732 children = Component(props, secondArg);
733 } while (didScheduleRenderPhaseUpdateDuringThisPass);
734 return children;
@@ -821,6 +814,7 @@ export function resetHooksOnUnwind(): void {
814 didScheduleRenderPhaseUpdateDuringThisPass = false;
815 localIdCounter = 0;
816 thenableIndexCounter = 0;
817 + thenableState = null;
818 }
819
820 function mountWorkInProgressHook(): Hook {
@@ -954,7 +948,11 @@ function use<T>(usable: Usable<T>): T {
948 // Track the position of the thenable within this fiber.
949 const index = thenableIndexCounter;
950 thenableIndexCounter += 1;
957 - return trackUsedThenable(thenable, index);
951 +
952 + if (thenableState === null) {
953 + thenableState = createThenableState();
954 + }
955 + return trackUsedThenable(thenableState, thenable, index);
956 } else if (
957 usable.$$typeof === REACT_CONTEXT_TYPE ||
958 usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-reconciler/src/ReactFiberThenable.new.js
+16 -33
@@ -31,29 +31,12 @@ export const SuspenseException: mixed = new Error(
31 "call the promise's `.catch` method and pass the result to `use`",
32 );
33
34 -let thenableState: ThenableState | null = null;
35 -
34 export function createThenableState(): ThenableState {
35 // The ThenableState is created the first time a component suspends. If it
36 // suspends again, we'll reuse the same state.
37 return [];
38 }
39
42 -export function prepareThenableState(prevThenableState: ThenableState | null) {
43 - // This function is called before every function that might suspend
44 - // with `use`. Right now, that's only Hooks, but in the future we'll use the
45 - // same mechanism for unwrapping promises during reconciliation.
46 - thenableState = prevThenableState;
47 -}
48 -
49 -export function getThenableStateAfterSuspending(): ThenableState | null {
50 - // Called by the work loop so it can stash the thenable state. It will use
51 - // the state to replay the component when the promise resolves.
52 - const state = thenableState;
53 - thenableState = null;
54 - return state;
55 -}
56 -
40 export function isThenableResolved(thenable: Thenable<mixed>): boolean {
41 const status = thenable.status;
42 return status === 'fulfilled' || status === 'rejected';
@@ -61,27 +44,27 @@ export function isThenableResolved(thenable: Thenable<mixed>): boolean {
44
45 function noop(): void {}
46
64 -export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
47 +export function trackUsedThenable<T>(
48 + thenableState: ThenableState,
49 + thenable: Thenable<T>,
50 + index: number,
51 +): T {
52 if (__DEV__ && ReactCurrentActQueue.current !== null) {
53 ReactCurrentActQueue.didUsePromise = true;
54 }
55
69 - if (thenableState === null) {
70 - thenableState = [thenable];
56 + const previous = thenableState[index];
57 + if (previous === undefined) {
58 + thenableState.push(thenable);
59 } else {
72 - const previous = thenableState[index];
73 - if (previous === undefined) {
74 - thenableState.push(thenable);
75 - } else {
76 - if (previous !== thenable) {
77 - // Reuse the previous thenable, and drop the new one. We can assume
78 - // they represent the same value, because components are idempotent.
79 -
80 - // Avoid an unhandled rejection errors for the Promises that we'll
81 - // intentionally ignore.
82 - thenable.then(noop, noop);
83 - thenable = previous;
84 - }
60 + if (previous !== thenable) {
61 + // Reuse the previous thenable, and drop the new one. We can assume
62 + // they represent the same value, because components are idempotent.
63 +
64 + // Avoid an unhandled rejection errors for the Promises that we'll
65 + // intentionally ignore.
66 + thenable.then(noop, noop);
67 + thenable = previous;
68 }
69 }
70
packages/react-reconciler/src/ReactFiberThenable.old.js
+16 -33
@@ -31,29 +31,12 @@ export const SuspenseException: mixed = new Error(
31 "call the promise's `.catch` method and pass the result to `use`",
32 );
33
34 -let thenableState: ThenableState | null = null;
35 -
34 export function createThenableState(): ThenableState {
35 // The ThenableState is created the first time a component suspends. If it
36 // suspends again, we'll reuse the same state.
37 return [];
38 }
39
42 -export function prepareThenableState(prevThenableState: ThenableState | null) {
43 - // This function is called before every function that might suspend
44 - // with `use`. Right now, that's only Hooks, but in the future we'll use the
45 - // same mechanism for unwrapping promises during reconciliation.
46 - thenableState = prevThenableState;
47 -}
48 -
49 -export function getThenableStateAfterSuspending(): ThenableState | null {
50 - // Called by the work loop so it can stash the thenable state. It will use
51 - // the state to replay the component when the promise resolves.
52 - const state = thenableState;
53 - thenableState = null;
54 - return state;
55 -}
56 -
40 export function isThenableResolved(thenable: Thenable<mixed>): boolean {
41 const status = thenable.status;
42 return status === 'fulfilled' || status === 'rejected';
@@ -61,27 +44,27 @@ export function isThenableResolved(thenable: Thenable<mixed>): boolean {
44
45 function noop(): void {}
46
64 -export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
47 +export function trackUsedThenable<T>(
48 + thenableState: ThenableState,
49 + thenable: Thenable<T>,
50 + index: number,
51 +): T {
52 if (__DEV__ && ReactCurrentActQueue.current !== null) {
53 ReactCurrentActQueue.didUsePromise = true;
54 }
55
69 - if (thenableState === null) {
70 - thenableState = [thenable];
56 + const previous = thenableState[index];
57 + if (previous === undefined) {
58 + thenableState.push(thenable);
59 } else {
72 - const previous = thenableState[index];
73 - if (previous === undefined) {
74 - thenableState.push(thenable);
75 - } else {
76 - if (previous !== thenable) {
77 - // Reuse the previous thenable, and drop the new one. We can assume
78 - // they represent the same value, because components are idempotent.
79 -
80 - // Avoid an unhandled rejection errors for the Promises that we'll
81 - // intentionally ignore.
82 - thenable.then(noop, noop);
83 - thenable = previous;
84 - }
60 + if (previous !== thenable) {
61 + // Reuse the previous thenable, and drop the new one. We can assume
62 + // they represent the same value, because components are idempotent.
63 +
64 + // Avoid an unhandled rejection errors for the Promises that we'll
65 + // intentionally ignore.
66 + thenable.then(noop, noop);
67 + thenable = previous;
68 }
69 }
70
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+43 -61
@@ -25,7 +25,6 @@ import type {
25 TransitionAbort,
26 } from './ReactFiberTracingMarkerComponent.new';
27 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
28 -import type {ThenableState} from './ReactFiberThenable.new';
28
29 import {
30 warnAboutDeprecatedLifecycles,
@@ -275,7 +274,6 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new
274 import {
275 SuspenseException,
276 getSuspendedThenable,
278 - getThenableStateAfterSuspending,
277 isThenableResolved,
278 } from './ReactFiberThenable.new';
279 import {schedulePostPaintCallback} from './ReactPostPaintCallback';
@@ -322,13 +320,14 @@ let workInProgress: Fiber | null = null;
320 // The lanes we're rendering
321 let workInProgressRootRenderLanes: Lanes = NoLanes;
322
325 -opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5;
323 +opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5 | 6;
324 const NotSuspended: SuspendedReason = 0;
325 const SuspendedOnError: SuspendedReason = 1;
326 const SuspendedOnData: SuspendedReason = 2;
327 const SuspendedOnImmediate: SuspendedReason = 3;
330 -const SuspendedAndReadyToUnwind: SuspendedReason = 4;
331 -const SuspendedOnHydration: SuspendedReason = 5;
328 +const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4;
329 +const SuspendedAndReadyToUnwind: SuspendedReason = 5;
330 +const SuspendedOnHydration: SuspendedReason = 6;
331
332 // When this is true, the work-in-progress fiber just suspended (or errored) and
333 // 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;
335 // immediately instead of unwinding the stack.
336 let workInProgressSuspendedReason: SuspendedReason = NotSuspended;
337 let workInProgressThrownValue: mixed = null;
339 -let workInProgressSuspendedThenableState: ThenableState | null = null;
338
339 // Whether a ping listener was attached during this render. This is slightly
340 // different that whether something suspended, because we don't add multiple
@@ -1749,7 +1747,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1747 workInProgressRootRenderLanes = renderLanes = lanes;
1748 workInProgressSuspendedReason = NotSuspended;
1749 workInProgressThrownValue = null;
1752 - workInProgressSuspendedThenableState = null;
1750 workInProgressRootDidAttachPingListener = false;
1751 workInProgressRootExitStatus = RootInProgress;
1752 workInProgressRootFatalError = null;
@@ -1802,7 +1799,6 @@ function handleThrow(root, thrownValue): void {
1799 // API for suspending. This implementation detail can change later, once we
1800 // deprecate the old API in favor of `use`.
1801 thrownValue = getSuspendedThenable();
1805 - workInProgressSuspendedThenableState = getThenableStateAfterSuspending();
1802 workInProgressSuspendedReason = shouldAttemptToSuspendUntilDataResolves()
1803 ? SuspendedOnData
1804 : SuspendedOnImmediate;
@@ -1816,13 +1812,9 @@ function handleThrow(root, thrownValue): void {
1812 //
1813 // We could name this something more general but as of now it's the only
1814 // case where we think this should happen.
1819 - workInProgressSuspendedThenableState = null;
1815 workInProgressSuspendedReason = SuspendedOnHydration;
1816 } else {
1822 - // This is a regular error. If something earlier in the component already
1823 - // suspended, we must clear the thenable state to unblock the work loop.
1824 - workInProgressSuspendedThenableState = null;
1825 -
1817 + // This is a regular error.
1818 const isWakeable =
1819 thrownValue !== null &&
1820 typeof thrownValue === 'object' &&
@@ -1832,7 +1824,7 @@ function handleThrow(root, thrownValue): void {
1824 workInProgressSuspendedReason = isWakeable
1825 ? // A wakeable object was thrown by a legacy Suspense implementation.
1826 // This has slightly different behavior than suspending with `use`.
1835 - SuspendedAndReadyToUnwind
1827 + SuspendedOnDeprecatedThrowPromise
1828 : // This is a regular error. If something earlier in the component already
1829 // suspended, we must clear the thenable state to unblock the work loop.
1830 SuspendedOnError;
@@ -2205,17 +2197,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2197 }
2198 case SuspendedOnData: {
2199 const thenable: Thenable<mixed> = (thrownValue: any);
2208 - if (workInProgressSuspendedThenableState !== null) {
2209 - const thenableState = workInProgressSuspendedThenableState;
2210 - if (isThenableResolved(thenable)) {
2211 - // The data resolved. Try rendering the component again.
2212 - workInProgressSuspendedReason = NotSuspended;
2213 - workInProgressThrownValue = null;
2214 - replaySuspendedUnitOfWork(unitOfWork, thenable, thenableState);
2215 - break;
2216 - }
2200 + if (isThenableResolved(thenable)) {
2201 + // The data resolved. Try rendering the component again.
2202 + workInProgressSuspendedReason = NotSuspended;
2203 + workInProgressThrownValue = null;
2204 + replaySuspendedUnitOfWork(unitOfWork);
2205 + break;
2206 }
2218 -
2207 // The work loop is suspended on data. We should wait for it to
2208 // resolve before continuing to render.
2209 const onResolution = () => {
@@ -2231,6 +2219,31 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2219 workInProgressSuspendedReason = SuspendedAndReadyToUnwind;
2220 break outer;
2221 }
2222 + case SuspendedAndReadyToUnwind: {
2223 + const thenable: Thenable<mixed> = (thrownValue: any);
2224 + if (isThenableResolved(thenable)) {
2225 + // The data resolved. Try rendering the component again.
2226 + workInProgressSuspendedReason = NotSuspended;
2227 + workInProgressThrownValue = null;
2228 + replaySuspendedUnitOfWork(unitOfWork);
2229 + } else {
2230 + // Otherwise, unwind then continue with the normal work loop.
2231 + workInProgressSuspendedReason = NotSuspended;
2232 + workInProgressThrownValue = null;
2233 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2234 + }
2235 + break;
2236 + }
2237 + case SuspendedOnDeprecatedThrowPromise: {
2238 + // Suspended by an old implementation that uses the `throw promise`
2239 + // pattern. The newer replaying behavior can cause subtle issues
2240 + // like infinite ping loops. So we maintain the old behavior and
2241 + // always unwind.
2242 + workInProgressSuspendedReason = NotSuspended;
2243 + workInProgressThrownValue = null;
2244 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2245 + break;
2246 + }
2247 case SuspendedOnHydration: {
2248 // Selective hydration. An update flowed into a dehydrated tree.
2249 // Interrupt the current render so the work loop can switch to the
@@ -2240,27 +2253,9 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2253 break outer;
2254 }
2255 default: {
2243 - if (workInProgressSuspendedThenableState !== null) {
2244 - const thenableState = workInProgressSuspendedThenableState;
2245 - const thenable: Thenable<mixed> = (thrownValue: any);
2246 - if (isThenableResolved(thenable)) {
2247 - // The data resolved. Try rendering the component again.
2248 - workInProgressSuspendedReason = NotSuspended;
2249 - workInProgressThrownValue = null;
2250 - replaySuspendedUnitOfWork(
2251 - unitOfWork,
2252 - thrownValue,
2253 - thenableState,
2254 - );
2255 - break;
2256 - }
2257 - }
2258 -
2259 - // Otherwise, unwind then continue with the normal work loop.
2260 - workInProgressSuspendedReason = NotSuspended;
2261 - workInProgressThrownValue = null;
2262 - unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2263 - break;
2256 + throw new Error(
2257 + 'Unexpected SuspendedReason. This is a bug in React.',
2258 + );
2259 }
2260 }
2261 }
@@ -2344,11 +2339,7 @@ function performUnitOfWork(unitOfWork: Fiber): void {
2339 ReactCurrentOwner.current = null;
2340 }
2341
2347 -function replaySuspendedUnitOfWork(
2348 - unitOfWork: Fiber,
2349 - thrownValue: mixed,
2350 - thenableState: ThenableState,
2351 -): void {
2342 +function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2343 // This is a fork of performUnitOfWork specifcally for replaying a fiber that
2344 // just suspended.
2345 //
@@ -2387,7 +2378,6 @@ function replaySuspendedUnitOfWork(
2378 unitOfWork,
2379 resolvedProps,
2380 Component,
2390 - thenableState,
2381 workInProgressRootRenderLanes,
2382 );
2383 break;
@@ -2400,7 +2390,6 @@ function replaySuspendedUnitOfWork(
2390 unitOfWork,
2391 nextProps,
2392 Component,
2403 - thenableState,
2393 workInProgressRootRenderLanes,
2394 );
2395 break;
@@ -2427,10 +2416,8 @@ function replaySuspendedUnitOfWork(
2416 stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2417 }
2418
2430 - // The begin phase finished successfully without suspending. Reset the state
2431 - // used to track the fiber while it was suspended. Then return to the normal
2432 - // work loop.
2433 - workInProgressSuspendedThenableState = null;
2419 + // The begin phase finished successfully without suspending. Return to the
2420 + // normal work loop.
2421
2422 resetCurrentDebugFiberInDEV();
2423 unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2450,7 +2437,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2437 //
2438 // Return to the normal work loop. This will unwind the stack, and potentially
2439 // result in showing a fallback.
2453 - workInProgressSuspendedThenableState = null;
2440 resetSuspendedWorkLoopOnUnwind();
2441
2442 const returnFiber = unitOfWork.return;
@@ -2494,10 +2480,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2480 completeUnitOfWork(unitOfWork);
2481 }
2482
2497 -export function getSuspendedThenableState(): ThenableState | null {
2498 - return workInProgressSuspendedThenableState;
2499 -}
2500 -
2483 function completeUnitOfWork(unitOfWork: Fiber): void {
2484 // Attempt to complete the current unit of work, then move to the next
2485 // sibling. If there are no more siblings, return to the parent fiber.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+43 -61
@@ -25,7 +25,6 @@ import type {
25 TransitionAbort,
26 } from './ReactFiberTracingMarkerComponent.old';
27 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
28 -import type {ThenableState} from './ReactFiberThenable.old';
28
29 import {
30 warnAboutDeprecatedLifecycles,
@@ -275,7 +274,6 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old
274 import {
275 SuspenseException,
276 getSuspendedThenable,
278 - getThenableStateAfterSuspending,
277 isThenableResolved,
278 } from './ReactFiberThenable.old';
279 import {schedulePostPaintCallback} from './ReactPostPaintCallback';
@@ -322,13 +320,14 @@ let workInProgress: Fiber | null = null;
320 // The lanes we're rendering
321 let workInProgressRootRenderLanes: Lanes = NoLanes;
322
325 -opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5;
323 +opaque type SuspendedReason = 0 | 1 | 2 | 3 | 4 | 5 | 6;
324 const NotSuspended: SuspendedReason = 0;
325 const SuspendedOnError: SuspendedReason = 1;
326 const SuspendedOnData: SuspendedReason = 2;
327 const SuspendedOnImmediate: SuspendedReason = 3;
330 -const SuspendedAndReadyToUnwind: SuspendedReason = 4;
331 -const SuspendedOnHydration: SuspendedReason = 5;
328 +const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4;
329 +const SuspendedAndReadyToUnwind: SuspendedReason = 5;
330 +const SuspendedOnHydration: SuspendedReason = 6;
331
332 // When this is true, the work-in-progress fiber just suspended (or errored) and
333 // 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;
335 // immediately instead of unwinding the stack.
336 let workInProgressSuspendedReason: SuspendedReason = NotSuspended;
337 let workInProgressThrownValue: mixed = null;
339 -let workInProgressSuspendedThenableState: ThenableState | null = null;
338
339 // Whether a ping listener was attached during this render. This is slightly
340 // different that whether something suspended, because we don't add multiple
@@ -1749,7 +1747,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1747 workInProgressRootRenderLanes = renderLanes = lanes;
1748 workInProgressSuspendedReason = NotSuspended;
1749 workInProgressThrownValue = null;
1752 - workInProgressSuspendedThenableState = null;
1750 workInProgressRootDidAttachPingListener = false;
1751 workInProgressRootExitStatus = RootInProgress;
1752 workInProgressRootFatalError = null;
@@ -1802,7 +1799,6 @@ function handleThrow(root, thrownValue): void {
1799 // API for suspending. This implementation detail can change later, once we
1800 // deprecate the old API in favor of `use`.
1801 thrownValue = getSuspendedThenable();
1805 - workInProgressSuspendedThenableState = getThenableStateAfterSuspending();
1802 workInProgressSuspendedReason = shouldAttemptToSuspendUntilDataResolves()
1803 ? SuspendedOnData
1804 : SuspendedOnImmediate;
@@ -1816,13 +1812,9 @@ function handleThrow(root, thrownValue): void {
1812 //
1813 // We could name this something more general but as of now it's the only
1814 // case where we think this should happen.
1819 - workInProgressSuspendedThenableState = null;
1815 workInProgressSuspendedReason = SuspendedOnHydration;
1816 } else {
1822 - // This is a regular error. If something earlier in the component already
1823 - // suspended, we must clear the thenable state to unblock the work loop.
1824 - workInProgressSuspendedThenableState = null;
1825 -
1817 + // This is a regular error.
1818 const isWakeable =
1819 thrownValue !== null &&
1820 typeof thrownValue === 'object' &&
@@ -1832,7 +1824,7 @@ function handleThrow(root, thrownValue): void {
1824 workInProgressSuspendedReason = isWakeable
1825 ? // A wakeable object was thrown by a legacy Suspense implementation.
1826 // This has slightly different behavior than suspending with `use`.
1835 - SuspendedAndReadyToUnwind
1827 + SuspendedOnDeprecatedThrowPromise
1828 : // This is a regular error. If something earlier in the component already
1829 // suspended, we must clear the thenable state to unblock the work loop.
1830 SuspendedOnError;
@@ -2205,17 +2197,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2197 }
2198 case SuspendedOnData: {
2199 const thenable: Thenable<mixed> = (thrownValue: any);
2208 - if (workInProgressSuspendedThenableState !== null) {
2209 - const thenableState = workInProgressSuspendedThenableState;
2210 - if (isThenableResolved(thenable)) {
2211 - // The data resolved. Try rendering the component again.
2212 - workInProgressSuspendedReason = NotSuspended;
2213 - workInProgressThrownValue = null;
2214 - replaySuspendedUnitOfWork(unitOfWork, thenable, thenableState);
2215 - break;
2216 - }
2200 + if (isThenableResolved(thenable)) {
2201 + // The data resolved. Try rendering the component again.
2202 + workInProgressSuspendedReason = NotSuspended;
2203 + workInProgressThrownValue = null;
2204 + replaySuspendedUnitOfWork(unitOfWork);
2205 + break;
2206 }
2218 -
2207 // The work loop is suspended on data. We should wait for it to
2208 // resolve before continuing to render.
2209 const onResolution = () => {
@@ -2231,6 +2219,31 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2219 workInProgressSuspendedReason = SuspendedAndReadyToUnwind;
2220 break outer;
2221 }
2222 + case SuspendedAndReadyToUnwind: {
2223 + const thenable: Thenable<mixed> = (thrownValue: any);
2224 + if (isThenableResolved(thenable)) {
2225 + // The data resolved. Try rendering the component again.
2226 + workInProgressSuspendedReason = NotSuspended;
2227 + workInProgressThrownValue = null;
2228 + replaySuspendedUnitOfWork(unitOfWork);
2229 + } else {
2230 + // Otherwise, unwind then continue with the normal work loop.
2231 + workInProgressSuspendedReason = NotSuspended;
2232 + workInProgressThrownValue = null;
2233 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2234 + }
2235 + break;
2236 + }
2237 + case SuspendedOnDeprecatedThrowPromise: {
2238 + // Suspended by an old implementation that uses the `throw promise`
2239 + // pattern. The newer replaying behavior can cause subtle issues
2240 + // like infinite ping loops. So we maintain the old behavior and
2241 + // always unwind.
2242 + workInProgressSuspendedReason = NotSuspended;
2243 + workInProgressThrownValue = null;
2244 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2245 + break;
2246 + }
2247 case SuspendedOnHydration: {
2248 // Selective hydration. An update flowed into a dehydrated tree.
2249 // Interrupt the current render so the work loop can switch to the
@@ -2240,27 +2253,9 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2253 break outer;
2254 }
2255 default: {
2243 - if (workInProgressSuspendedThenableState !== null) {
2244 - const thenableState = workInProgressSuspendedThenableState;
2245 - const thenable: Thenable<mixed> = (thrownValue: any);
2246 - if (isThenableResolved(thenable)) {
2247 - // The data resolved. Try rendering the component again.
2248 - workInProgressSuspendedReason = NotSuspended;
2249 - workInProgressThrownValue = null;
2250 - replaySuspendedUnitOfWork(
2251 - unitOfWork,
2252 - thrownValue,
2253 - thenableState,
2254 - );
2255 - break;
2256 - }
2257 - }
2258 -
2259 - // Otherwise, unwind then continue with the normal work loop.
2260 - workInProgressSuspendedReason = NotSuspended;
2261 - workInProgressThrownValue = null;
2262 - unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2263 - break;
2256 + throw new Error(
2257 + 'Unexpected SuspendedReason. This is a bug in React.',
2258 + );
2259 }
2260 }
2261 }
@@ -2344,11 +2339,7 @@ function performUnitOfWork(unitOfWork: Fiber): void {
2339 ReactCurrentOwner.current = null;
2340 }
2341
2347 -function replaySuspendedUnitOfWork(
2348 - unitOfWork: Fiber,
2349 - thrownValue: mixed,
2350 - thenableState: ThenableState,
2351 -): void {
2342 +function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2343 // This is a fork of performUnitOfWork specifcally for replaying a fiber that
2344 // just suspended.
2345 //
@@ -2387,7 +2378,6 @@ function replaySuspendedUnitOfWork(
2378 unitOfWork,
2379 resolvedProps,
2380 Component,
2390 - thenableState,
2381 workInProgressRootRenderLanes,
2382 );
2383 break;
@@ -2400,7 +2390,6 @@ function replaySuspendedUnitOfWork(
2390 unitOfWork,
2391 nextProps,
2392 Component,
2403 - thenableState,
2393 workInProgressRootRenderLanes,
2394 );
2395 break;
@@ -2427,10 +2416,8 @@ function replaySuspendedUnitOfWork(
2416 stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2417 }
2418
2430 - // The begin phase finished successfully without suspending. Reset the state
2431 - // used to track the fiber while it was suspended. Then return to the normal
2432 - // work loop.
2433 - workInProgressSuspendedThenableState = null;
2419 + // The begin phase finished successfully without suspending. Return to the
2420 + // normal work loop.
2421
2422 resetCurrentDebugFiberInDEV();
2423 unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2450,7 +2437,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2437 //
2438 // Return to the normal work loop. This will unwind the stack, and potentially
2439 // result in showing a fallback.
2453 - workInProgressSuspendedThenableState = null;
2440 resetSuspendedWorkLoopOnUnwind();
2441
2442 const returnFiber = unitOfWork.return;
@@ -2494,10 +2480,6 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2480 completeUnitOfWork(unitOfWork);
2481 }
2482
2497 -export function getSuspendedThenableState(): ThenableState | null {
2498 - return workInProgressSuspendedThenableState;
2499 -}
2500 -
2483 function completeUnitOfWork(unitOfWork: Fiber): void {
2484 // Attempt to complete the current unit of work, then move to the next
2485 // sibling. If there are no more siblings, return to the parent fiber.
scripts/error-codes/codes.json
+2 -1
@@ -446,5 +446,6 @@
446 "458": "Currently React only supports one RSC renderer at a time.",
447 "459": "Expected a suspended thenable. This is a bug in React. Please file an issue.",
448 "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`",
449 - "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."
449 + "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.",
450 + "462": "Unexpected SuspendedReason. This is a bug in React."
451 }