Skip to content

Commit 7572e49

Browse files
committed
Track thenable state in work loop
This is a refactor to track the array of thenables that is preserved across replays in the work loop instead of the Thenable module. The reason is that I'm about to add additional state to the Thenable module that is specific to a particular attempt — like the current index — and is reset between replays. So it's helpful to keep the two kinds of state separate so it's clearer which state gets reset when. The array of thenables is not reset until the work-in-progress either completes or unwinds. This also makes the structure more similar to Fizz and Flight.
1 parent 7fc3eef commit 7572e49

7 files changed

+201
-71
lines changed

packages/react-reconciler/src/ReactFiberHooks.new.js

+6
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import {
102102
requestEventTime,
103103
markSkippedUpdateLanes,
104104
isInvalidExecutionContextForEventFunction,
105+
getSuspendedThenableState,
105106
} from './ReactFiberWorkLoop.new';
106107

107108
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -134,6 +135,7 @@ import {
134135
import {getTreeId} from './ReactFiberTreeContext.new';
135136
import {now} from './Scheduler';
136137
import {
138+
prepareThenableState,
137139
trackUsedThenable,
138140
getPreviouslyUsedThenableAtIndex,
139141
} from './ReactFiberThenable.new';
@@ -465,6 +467,9 @@ export function renderWithHooks<Props, SecondArg>(
465467
: HooksDispatcherOnUpdate;
466468
}
467469

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

470475
// Check if there was a render phase update
@@ -506,6 +511,7 @@ export function renderWithHooks<Props, SecondArg>(
506511
? HooksDispatcherOnRerenderInDEV
507512
: HooksDispatcherOnRerender;
508513

514+
prepareThenableState(prevThenableState);
509515
children = Component(props, secondArg);
510516
} while (didScheduleRenderPhaseUpdateDuringThisPass);
511517
}

packages/react-reconciler/src/ReactFiberHooks.old.js

+6
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import {
102102
requestEventTime,
103103
markSkippedUpdateLanes,
104104
isInvalidExecutionContextForEventFunction,
105+
getSuspendedThenableState,
105106
} from './ReactFiberWorkLoop.old';
106107

107108
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -134,6 +135,7 @@ import {
134135
import {getTreeId} from './ReactFiberTreeContext.old';
135136
import {now} from './Scheduler';
136137
import {
138+
prepareThenableState,
137139
trackUsedThenable,
138140
getPreviouslyUsedThenableAtIndex,
139141
} from './ReactFiberThenable.old';
@@ -465,6 +467,9 @@ export function renderWithHooks<Props, SecondArg>(
465467
: HooksDispatcherOnUpdate;
466468
}
467469

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

470475
// Check if there was a render phase update
@@ -506,6 +511,7 @@ export function renderWithHooks<Props, SecondArg>(
506511
? HooksDispatcherOnRerenderInDEV
507512
: HooksDispatcherOnRerender;
508513

514+
prepareThenableState(prevThenableState);
509515
children = Component(props, secondArg);
510516
} while (didScheduleRenderPhaseUpdateDuringThisPass);
511517
}

packages/react-reconciler/src/ReactFiberThenable.new.js

+43-24
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,62 @@ import type {
1717
import ReactSharedInternals from 'shared/ReactSharedInternals';
1818
const {ReactCurrentActQueue} = ReactSharedInternals;
1919

20-
let suspendedThenable: Thenable<any> | null = null;
21-
let usedThenables: Array<Thenable<any> | void> | null = null;
20+
// TODO: Sparse arrays are bad for performance.
21+
export opaque type ThenableState = Array<Thenable<any> | void>;
2222

23-
export function isTrackingSuspendedThenable(): boolean {
24-
return suspendedThenable !== null;
23+
let thenableState: ThenableState | null = null;
24+
25+
export function createThenableState(): ThenableState {
26+
// The ThenableState is created the first time a component suspends. If it
27+
// suspends again, we'll reuse the same state.
28+
return [];
29+
}
30+
31+
export function prepareThenableState(prevThenableState: ThenableState | null) {
32+
// This function is called before every function that might suspend
33+
// with `use`. Right now, that's only Hooks, but in the future we'll use the
34+
// same mechanism for unwrapping promises during reconciliation.
35+
thenableState = prevThenableState;
36+
}
37+
38+
export function getThenableStateAfterSuspending(): ThenableState | null {
39+
// Called by the work loop so it can stash the thenable state. It will use
40+
// the state to replay the component when the promise resolves.
41+
if (
42+
thenableState !== null &&
43+
// If we only `use`-ed resolved promises, then there is no suspended state
44+
// TODO: The only reason we do this is to distinguish between throwing a
45+
// promise (old Suspense pattern) versus `use`-ing one. A better solution is
46+
// for `use` to throw a special, opaque value instead of a promise.
47+
!isThenableStateResolved(thenableState)
48+
) {
49+
const state = thenableState;
50+
thenableState = null;
51+
return state;
52+
}
53+
return null;
2554
}
2655

27-
export function suspendedThenableDidResolve(): boolean {
28-
if (suspendedThenable !== null) {
29-
const status = suspendedThenable.status;
56+
export function isThenableStateResolved(thenables: ThenableState): boolean {
57+
const lastThenable = thenables[thenables.length - 1];
58+
if (lastThenable !== undefined) {
59+
const status = lastThenable.status;
3060
return status === 'fulfilled' || status === 'rejected';
3161
}
32-
return false;
62+
return true;
3363
}
3464

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

40-
if (usedThenables === null) {
41-
usedThenables = [thenable];
70+
if (thenableState === null) {
71+
thenableState = [thenable];
4272
} else {
43-
usedThenables[index] = thenable;
73+
thenableState[index] = thenable;
4474
}
4575

46-
suspendedThenable = thenable;
47-
4876
// We use an expando to track the status and result of a thenable so that we
4977
// can synchronously unwrap the value. Think of this as an extension of the
5078
// Promise API, or a custom interface that is a superset of Thenable.
@@ -59,7 +87,6 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
5987
// this thenable, because if we keep trying it will likely infinite loop
6088
// without ever resolving.
6189
// TODO: Log a warning?
62-
suspendedThenable = null;
6390
break;
6491
default: {
6592
if (typeof thenable.status === 'string') {
@@ -91,19 +118,11 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
91118
}
92119
}
93120

94-
export function resetWakeableStateAfterEachAttempt() {
95-
suspendedThenable = null;
96-
}
97-
98-
export function resetThenableStateOnCompletion() {
99-
usedThenables = null;
100-
}
101-
102121
export function getPreviouslyUsedThenableAtIndex<T>(
103122
index: number,
104123
): Thenable<T> | null {
105-
if (usedThenables !== null) {
106-
const thenable = usedThenables[index];
124+
if (thenableState !== null) {
125+
const thenable = thenableState[index];
107126
if (thenable !== undefined) {
108127
return thenable;
109128
}

packages/react-reconciler/src/ReactFiberThenable.old.js

+43-24
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,62 @@ import type {
1717
import ReactSharedInternals from 'shared/ReactSharedInternals';
1818
const {ReactCurrentActQueue} = ReactSharedInternals;
1919

20-
let suspendedThenable: Thenable<any> | null = null;
21-
let usedThenables: Array<Thenable<any> | void> | null = null;
20+
// TODO: Sparse arrays are bad for performance.
21+
export opaque type ThenableState = Array<Thenable<any> | void>;
2222

23-
export function isTrackingSuspendedThenable(): boolean {
24-
return suspendedThenable !== null;
23+
let thenableState: ThenableState | null = null;
24+
25+
export function createThenableState(): ThenableState {
26+
// The ThenableState is created the first time a component suspends. If it
27+
// suspends again, we'll reuse the same state.
28+
return [];
29+
}
30+
31+
export function prepareThenableState(prevThenableState: ThenableState | null) {
32+
// This function is called before every function that might suspend
33+
// with `use`. Right now, that's only Hooks, but in the future we'll use the
34+
// same mechanism for unwrapping promises during reconciliation.
35+
thenableState = prevThenableState;
36+
}
37+
38+
export function getThenableStateAfterSuspending(): ThenableState | null {
39+
// Called by the work loop so it can stash the thenable state. It will use
40+
// the state to replay the component when the promise resolves.
41+
if (
42+
thenableState !== null &&
43+
// If we only `use`-ed resolved promises, then there is no suspended state
44+
// TODO: The only reason we do this is to distinguish between throwing a
45+
// promise (old Suspense pattern) versus `use`-ing one. A better solution is
46+
// for `use` to throw a special, opaque value instead of a promise.
47+
!isThenableStateResolved(thenableState)
48+
) {
49+
const state = thenableState;
50+
thenableState = null;
51+
return state;
52+
}
53+
return null;
2554
}
2655

27-
export function suspendedThenableDidResolve(): boolean {
28-
if (suspendedThenable !== null) {
29-
const status = suspendedThenable.status;
56+
export function isThenableStateResolved(thenables: ThenableState): boolean {
57+
const lastThenable = thenables[thenables.length - 1];
58+
if (lastThenable !== undefined) {
59+
const status = lastThenable.status;
3060
return status === 'fulfilled' || status === 'rejected';
3161
}
32-
return false;
62+
return true;
3363
}
3464

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

40-
if (usedThenables === null) {
41-
usedThenables = [thenable];
70+
if (thenableState === null) {
71+
thenableState = [thenable];
4272
} else {
43-
usedThenables[index] = thenable;
73+
thenableState[index] = thenable;
4474
}
4575

46-
suspendedThenable = thenable;
47-
4876
// We use an expando to track the status and result of a thenable so that we
4977
// can synchronously unwrap the value. Think of this as an extension of the
5078
// Promise API, or a custom interface that is a superset of Thenable.
@@ -59,7 +87,6 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
5987
// this thenable, because if we keep trying it will likely infinite loop
6088
// without ever resolving.
6189
// TODO: Log a warning?
62-
suspendedThenable = null;
6390
break;
6491
default: {
6592
if (typeof thenable.status === 'string') {
@@ -91,19 +118,11 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
91118
}
92119
}
93120

94-
export function resetWakeableStateAfterEachAttempt() {
95-
suspendedThenable = null;
96-
}
97-
98-
export function resetThenableStateOnCompletion() {
99-
usedThenables = null;
100-
}
101-
102121
export function getPreviouslyUsedThenableAtIndex<T>(
103122
index: number,
104123
): Thenable<T> | null {
105-
if (usedThenables !== null) {
106-
const thenable = usedThenables[index];
124+
if (thenableState !== null) {
125+
const thenable = thenableState[index];
107126
if (thenable !== undefined) {
108127
return thenable;
109128
}

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+16-11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
TransitionAbort,
2323
} from './ReactFiberTracingMarkerComponent.new';
2424
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
25+
import type {ThenableState} from './ReactFiberThenable.new';
2526

2627
import {
2728
warnAboutDeprecatedLifecycles,
@@ -265,10 +266,8 @@ import {
265266
} from './ReactFiberAct.new';
266267
import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new';
267268
import {
268-
resetWakeableStateAfterEachAttempt,
269-
resetThenableStateOnCompletion,
270-
suspendedThenableDidResolve,
271-
isTrackingSuspendedThenable,
269+
getThenableStateAfterSuspending,
270+
isThenableStateResolved,
272271
} from './ReactFiberThenable.new';
273272
import {schedulePostPaintCallback} from './ReactPostPaintCallback';
274273

@@ -315,6 +314,7 @@ let workInProgressRootRenderLanes: Lanes = NoLanes;
315314
// immediately instead of unwinding the stack.
316315
let workInProgressIsSuspended: boolean = false;
317316
let workInProgressThrownValue: mixed = null;
317+
let workInProgressSuspendedThenableState: ThenableState | null = null;
318318

319319
// Whether a ping listener was attached during this render. This is slightly
320320
// different that whether something suspended, because we don't add multiple
@@ -1686,15 +1686,14 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
16861686
);
16871687
interruptedWork = interruptedWork.return;
16881688
}
1689-
resetWakeableStateAfterEachAttempt();
1690-
resetThenableStateOnCompletion();
16911689
}
16921690
workInProgressRoot = root;
16931691
const rootWorkInProgress = createWorkInProgress(root.current, null);
16941692
workInProgress = rootWorkInProgress;
16951693
workInProgressRootRenderLanes = renderLanes = lanes;
16961694
workInProgressIsSuspended = false;
16971695
workInProgressThrownValue = null;
1696+
workInProgressSuspendedThenableState = null;
16981697
workInProgressRootDidAttachPingListener = false;
16991698
workInProgressRootExitStatus = RootInProgress;
17001699
workInProgressRootFatalError = null;
@@ -1729,6 +1728,7 @@ function handleThrow(root, thrownValue): void {
17291728
// as suspending the execution of the work loop.
17301729
workInProgressIsSuspended = true;
17311730
workInProgressThrownValue = thrownValue;
1731+
workInProgressSuspendedThenableState = getThenableStateAfterSuspending();
17321732

17331733
const erroredWork = workInProgress;
17341734
if (erroredWork === null) {
@@ -2014,7 +2014,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
20142014
break;
20152015
} catch (thrownValue) {
20162016
handleThrow(root, thrownValue);
2017-
if (isTrackingSuspendedThenable()) {
2017+
if (workInProgressSuspendedThenableState !== null) {
20182018
// If this fiber just suspended, it's possible the data is already
20192019
// cached. Yield to the main thread to give it a chance to ping. If
20202020
// it does, we can retry immediately without unwinding the stack.
@@ -2117,13 +2117,14 @@ function resumeSuspendedUnitOfWork(
21172117
// instead of unwinding the stack. It's a separate function to keep the
21182118
// additional logic out of the work loop's hot path.
21192119

2120-
const wasPinged = suspendedThenableDidResolve();
2121-
resetWakeableStateAfterEachAttempt();
2120+
const wasPinged =
2121+
workInProgressSuspendedThenableState !== null &&
2122+
isThenableStateResolved(workInProgressSuspendedThenableState);
21222123

21232124
if (!wasPinged) {
21242125
// The thenable wasn't pinged. Return to the normal work loop. This will
21252126
// unwind the stack, and potentially result in showing a fallback.
2126-
resetThenableStateOnCompletion();
2127+
workInProgressSuspendedThenableState = null;
21272128

21282129
const returnFiber = unitOfWork.return;
21292130
if (returnFiber === null || workInProgressRoot === null) {
@@ -2188,7 +2189,7 @@ function resumeSuspendedUnitOfWork(
21882189
// The begin phase finished successfully without suspending. Reset the state
21892190
// used to track the fiber while it was suspended. Then return to the normal
21902191
// work loop.
2191-
resetThenableStateOnCompletion();
2192+
workInProgressSuspendedThenableState = null;
21922193

21932194
resetCurrentDebugFiberInDEV();
21942195
unitOfWork.memoizedProps = unitOfWork.pendingProps;
@@ -2202,6 +2203,10 @@ function resumeSuspendedUnitOfWork(
22022203
ReactCurrentOwner.current = null;
22032204
}
22042205

2206+
export function getSuspendedThenableState(): ThenableState | null {
2207+
return workInProgressSuspendedThenableState;
2208+
}
2209+
22052210
function completeUnitOfWork(unitOfWork: Fiber): void {
22062211
// Attempt to complete the current unit of work, then move to the next
22072212
// sibling. If there are no more siblings, return to the parent fiber.

0 commit comments

Comments
 (0)