Skip to content

Commit f284d9f

Browse files
committed
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.
1 parent 6b4c031 commit f284d9f

File tree

9 files changed

+142
-221
lines changed

9 files changed

+142
-221
lines changed

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import type {
3838
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
3939
import type {RootState} from './ReactFiberRoot.new';
4040
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
41-
import type {ThenableState} from './ReactFiberThenable.new';
4241

4342
import checkPropTypes from 'shared/checkPropTypes';
4443
import {
@@ -1167,7 +1166,6 @@ export function replayFunctionComponent(
11671166
workInProgress: Fiber,
11681167
nextProps: any,
11691168
Component: any,
1170-
prevThenableState: ThenableState,
11711169
renderLanes: Lanes,
11721170
): Fiber | null {
11731171
// This function is used to replay a component that previously suspended,
@@ -1190,7 +1188,6 @@ export function replayFunctionComponent(
11901188
Component,
11911189
nextProps,
11921190
context,
1193-
prevThenableState,
11941191
);
11951192
const hasId = checkDidRenderIdHook();
11961193
if (enableSchedulingProfiler) {

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import type {
3838
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
3939
import type {RootState} from './ReactFiberRoot.old';
4040
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
41-
import type {ThenableState} from './ReactFiberThenable.old';
4241

4342
import checkPropTypes from 'shared/checkPropTypes';
4443
import {
@@ -1167,7 +1166,6 @@ export function replayFunctionComponent(
11671166
workInProgress: Fiber,
11681167
nextProps: any,
11691168
Component: any,
1170-
prevThenableState: ThenableState,
11711169
renderLanes: Lanes,
11721170
): Fiber | null {
11731171
// This function is used to replay a component that previously suspended,
@@ -1190,7 +1188,6 @@ export function replayFunctionComponent(
11901188
Component,
11911189
nextProps,
11921190
context,
1193-
prevThenableState,
11941191
);
11951192
const hasId = checkDidRenderIdHook();
11961193
if (enableSchedulingProfiler) {

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ import {
105105
requestEventTime,
106106
markSkippedUpdateLanes,
107107
isInvalidExecutionContextForEventFunction,
108-
getSuspendedThenableState,
109108
} from './ReactFiberWorkLoop.new';
110109

111110
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -141,9 +140,9 @@ import {
141140
import {getTreeId} from './ReactFiberTreeContext.new';
142141
import {now} from './Scheduler';
143142
import {
144-
prepareThenableState,
145143
trackUsedThenable,
146144
checkIfUseWrappedInTryCatch,
145+
createThenableState,
147146
} from './ReactFiberThenable.new';
148147
import type {ThenableState} from './ReactFiberThenable.new';
149148

@@ -247,6 +246,7 @@ let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
247246
let localIdCounter: number = 0;
248247
// Counts number of `use`-d thenables
249248
let thenableIndexCounter: number = 0;
249+
let thenableState: ThenableState | null = null;
250250

251251
// Used for ids that are generated completely client-side (i.e. not during
252252
// hydration). This counter is global, so client ids are not stable across
@@ -449,6 +449,7 @@ export function renderWithHooks<Props, SecondArg>(
449449
// didScheduleRenderPhaseUpdate = false;
450450
// localIdCounter = 0;
451451
// thenableIndexCounter = 0;
452+
// thenableState = null;
452453

453454
// TODO Warn if no hooks are used at all during mount, then some are used during update.
454455
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -477,10 +478,6 @@ export function renderWithHooks<Props, SecondArg>(
477478
: HooksDispatcherOnUpdate;
478479
}
479480

480-
// If this is a replay, restore the thenable state from the previous attempt.
481-
const prevThenableState = getSuspendedThenableState();
482-
prepareThenableState(prevThenableState);
483-
484481
// In Strict Mode, during development, user functions are double invoked to
485482
// help detect side effects. The logic for how this is implemented for in
486483
// hook components is a bit complex so let's break it down.
@@ -525,7 +522,6 @@ export function renderWithHooks<Props, SecondArg>(
525522
Component,
526523
props,
527524
secondArg,
528-
prevThenableState,
529525
);
530526
}
531527

@@ -538,7 +534,6 @@ export function renderWithHooks<Props, SecondArg>(
538534
Component,
539535
props,
540536
secondArg,
541-
prevThenableState,
542537
);
543538
} finally {
544539
setIsStrictModeForDevtools(false);
@@ -600,7 +595,9 @@ function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
600595
didScheduleRenderPhaseUpdate = false;
601596
// This is reset by checkDidRenderIdHook
602597
// localIdCounter = 0;
598+
603599
thenableIndexCounter = 0;
600+
thenableState = null;
604601

605602
if (didRenderTooFewHooks) {
606603
throw new Error(
@@ -652,7 +649,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
652649
Component: (p: Props, arg: SecondArg) => any,
653650
props: Props,
654651
secondArg: SecondArg,
655-
prevThenableState: ThenableState | null,
656652
): any {
657653
// This function is used to replay a component that previously suspended,
658654
// after its data resolves.
@@ -676,7 +672,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
676672
Component,
677673
props,
678674
secondArg,
679-
prevThenableState,
680675
);
681676
finishRenderingHooks(current, workInProgress);
682677
return children;
@@ -687,7 +682,6 @@ function renderWithHooksAgain<Props, SecondArg>(
687682
Component: (p: Props, arg: SecondArg) => any,
688683
props: Props,
689684
secondArg: SecondArg,
690-
prevThenableState: ThenableState | null,
691685
) {
692686
// This is used to perform another render pass. It's used when setState is
693687
// called during render, and for double invoking components in Strict Mode
@@ -735,7 +729,6 @@ function renderWithHooksAgain<Props, SecondArg>(
735729
? HooksDispatcherOnRerenderInDEV
736730
: HooksDispatcherOnRerender;
737731

738-
prepareThenableState(prevThenableState);
739732
children = Component(props, secondArg);
740733
} while (didScheduleRenderPhaseUpdateDuringThisPass);
741734
return children;
@@ -821,6 +814,7 @@ export function resetHooksOnUnwind(): void {
821814
didScheduleRenderPhaseUpdateDuringThisPass = false;
822815
localIdCounter = 0;
823816
thenableIndexCounter = 0;
817+
thenableState = null;
824818
}
825819

826820
function mountWorkInProgressHook(): Hook {
@@ -954,7 +948,11 @@ function use<T>(usable: Usable<T>): T {
954948
// Track the position of the thenable within this fiber.
955949
const index = thenableIndexCounter;
956950
thenableIndexCounter += 1;
957-
return trackUsedThenable(thenable, index);
951+
952+
if (thenableState === null) {
953+
thenableState = createThenableState();
954+
}
955+
return trackUsedThenable(thenableState, thenable, index);
958956
} else if (
959957
usable.$$typeof === REACT_CONTEXT_TYPE ||
960958
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ import {
105105
requestEventTime,
106106
markSkippedUpdateLanes,
107107
isInvalidExecutionContextForEventFunction,
108-
getSuspendedThenableState,
109108
} from './ReactFiberWorkLoop.old';
110109

111110
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -141,9 +140,9 @@ import {
141140
import {getTreeId} from './ReactFiberTreeContext.old';
142141
import {now} from './Scheduler';
143142
import {
144-
prepareThenableState,
145143
trackUsedThenable,
146144
checkIfUseWrappedInTryCatch,
145+
createThenableState,
147146
} from './ReactFiberThenable.old';
148147
import type {ThenableState} from './ReactFiberThenable.old';
149148

@@ -247,6 +246,7 @@ let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;
247246
let localIdCounter: number = 0;
248247
// Counts number of `use`-d thenables
249248
let thenableIndexCounter: number = 0;
249+
let thenableState: ThenableState | null = null;
250250

251251
// Used for ids that are generated completely client-side (i.e. not during
252252
// hydration). This counter is global, so client ids are not stable across
@@ -449,6 +449,7 @@ export function renderWithHooks<Props, SecondArg>(
449449
// didScheduleRenderPhaseUpdate = false;
450450
// localIdCounter = 0;
451451
// thenableIndexCounter = 0;
452+
// thenableState = null;
452453

453454
// TODO Warn if no hooks are used at all during mount, then some are used during update.
454455
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -477,10 +478,6 @@ export function renderWithHooks<Props, SecondArg>(
477478
: HooksDispatcherOnUpdate;
478479
}
479480

480-
// If this is a replay, restore the thenable state from the previous attempt.
481-
const prevThenableState = getSuspendedThenableState();
482-
prepareThenableState(prevThenableState);
483-
484481
// In Strict Mode, during development, user functions are double invoked to
485482
// help detect side effects. The logic for how this is implemented for in
486483
// hook components is a bit complex so let's break it down.
@@ -525,7 +522,6 @@ export function renderWithHooks<Props, SecondArg>(
525522
Component,
526523
props,
527524
secondArg,
528-
prevThenableState,
529525
);
530526
}
531527

@@ -538,7 +534,6 @@ export function renderWithHooks<Props, SecondArg>(
538534
Component,
539535
props,
540536
secondArg,
541-
prevThenableState,
542537
);
543538
} finally {
544539
setIsStrictModeForDevtools(false);
@@ -600,7 +595,9 @@ function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
600595
didScheduleRenderPhaseUpdate = false;
601596
// This is reset by checkDidRenderIdHook
602597
// localIdCounter = 0;
598+
603599
thenableIndexCounter = 0;
600+
thenableState = null;
604601

605602
if (didRenderTooFewHooks) {
606603
throw new Error(
@@ -652,7 +649,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
652649
Component: (p: Props, arg: SecondArg) => any,
653650
props: Props,
654651
secondArg: SecondArg,
655-
prevThenableState: ThenableState | null,
656652
): any {
657653
// This function is used to replay a component that previously suspended,
658654
// after its data resolves.
@@ -676,7 +672,6 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
676672
Component,
677673
props,
678674
secondArg,
679-
prevThenableState,
680675
);
681676
finishRenderingHooks(current, workInProgress);
682677
return children;
@@ -687,7 +682,6 @@ function renderWithHooksAgain<Props, SecondArg>(
687682
Component: (p: Props, arg: SecondArg) => any,
688683
props: Props,
689684
secondArg: SecondArg,
690-
prevThenableState: ThenableState | null,
691685
) {
692686
// This is used to perform another render pass. It's used when setState is
693687
// called during render, and for double invoking components in Strict Mode
@@ -735,7 +729,6 @@ function renderWithHooksAgain<Props, SecondArg>(
735729
? HooksDispatcherOnRerenderInDEV
736730
: HooksDispatcherOnRerender;
737731

738-
prepareThenableState(prevThenableState);
739732
children = Component(props, secondArg);
740733
} while (didScheduleRenderPhaseUpdateDuringThisPass);
741734
return children;
@@ -821,6 +814,7 @@ export function resetHooksOnUnwind(): void {
821814
didScheduleRenderPhaseUpdateDuringThisPass = false;
822815
localIdCounter = 0;
823816
thenableIndexCounter = 0;
817+
thenableState = null;
824818
}
825819

826820
function mountWorkInProgressHook(): Hook {
@@ -954,7 +948,11 @@ function use<T>(usable: Usable<T>): T {
954948
// Track the position of the thenable within this fiber.
955949
const index = thenableIndexCounter;
956950
thenableIndexCounter += 1;
957-
return trackUsedThenable(thenable, index);
951+
952+
if (thenableState === null) {
953+
thenableState = createThenableState();
954+
}
955+
return trackUsedThenable(thenableState, thenable, index);
958956
} else if (
959957
usable.$$typeof === REACT_CONTEXT_TYPE ||
960958
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE

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

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,57 +31,40 @@ export const SuspenseException: mixed = new Error(
3131
"call the promise's `.catch` method and pass the result to `use`",
3232
);
3333

34-
let thenableState: ThenableState | null = null;
35-
3634
export function createThenableState(): ThenableState {
3735
// The ThenableState is created the first time a component suspends. If it
3836
// suspends again, we'll reuse the same state.
3937
return [];
4038
}
4139

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-
5740
export function isThenableResolved(thenable: Thenable<mixed>): boolean {
5841
const status = thenable.status;
5942
return status === 'fulfilled' || status === 'rejected';
6043
}
6144

6245
function noop(): void {}
6346

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 {
6552
if (__DEV__ && ReactCurrentActQueue.current !== null) {
6653
ReactCurrentActQueue.didUsePromise = true;
6754
}
6855

69-
if (thenableState === null) {
70-
thenableState = [thenable];
56+
const previous = thenableState[index];
57+
if (previous === undefined) {
58+
thenableState.push(thenable);
7159
} 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;
8568
}
8669
}
8770

0 commit comments

Comments
 (0)