Skip to content

Commit 513f382

Browse files
committed
Reuse hooks when replaying a suspended component
When a component suspends, under some conditions, we can wait for the data to resolve and replay the component without unwinding the stack or showing a fallback in the interim. When we do this, we reuse the promises that were unwrapped during the previous attempts, so that if they aren't memoized, the result can still be used. We should do the same for all hooks. That way, if you _do_ memoize an async function call with useMemo, it won't be called again during the replay. This effectively gives you a local version of the functionality provided by `cache`, using the normal memoization patterns that have long existed in React.
1 parent 669a321 commit 513f382

File tree

7 files changed

+418
-22
lines changed

7 files changed

+418
-22
lines changed

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ 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';
42+
4143
import checkPropTypes from 'shared/checkPropTypes';
4244
import {
4345
markComponentRenderStarted,
@@ -203,6 +205,7 @@ import {
203205
renderWithHooks,
204206
checkDidRenderIdHook,
205207
bailoutHooks,
208+
replaySuspendedComponentWithHooks,
206209
} from './ReactFiberHooks.new';
207210
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.new';
208211
import {
@@ -1137,6 +1140,56 @@ function updateFunctionComponent(
11371140
return workInProgress.child;
11381141
}
11391142

1143+
export function replayFunctionComponent(
1144+
current: Fiber | null,
1145+
workInProgress: Fiber,
1146+
nextProps: any,
1147+
Component: any,
1148+
prevThenableState: ThenableState,
1149+
renderLanes: Lanes,
1150+
): Fiber | null {
1151+
// This function is used to replay a component that previously suspended,
1152+
// after its data resolves. It's a simplified version of
1153+
// updateFunctionComponent that reuses the hooks from the previous attempt.
1154+
1155+
let context;
1156+
if (!disableLegacyContext) {
1157+
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1158+
context = getMaskedContext(workInProgress, unmaskedContext);
1159+
}
1160+
1161+
prepareToReadContext(workInProgress, renderLanes);
1162+
if (enableSchedulingProfiler) {
1163+
markComponentRenderStarted(workInProgress);
1164+
}
1165+
const nextChildren = replaySuspendedComponentWithHooks(
1166+
current,
1167+
workInProgress,
1168+
Component,
1169+
nextProps,
1170+
context,
1171+
prevThenableState,
1172+
);
1173+
const hasId = checkDidRenderIdHook();
1174+
if (enableSchedulingProfiler) {
1175+
markComponentRenderStopped();
1176+
}
1177+
1178+
if (current !== null && !didReceiveUpdate) {
1179+
bailoutHooks(current, workInProgress, renderLanes);
1180+
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1181+
}
1182+
1183+
if (getIsHydrating() && hasId) {
1184+
pushMaterializedTreeId(workInProgress);
1185+
}
1186+
1187+
// React DevTools reads this flag.
1188+
workInProgress.flags |= PerformedWork;
1189+
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1190+
return workInProgress.child;
1191+
}
1192+
11401193
function updateClassComponent(
11411194
current: Fiber | null,
11421195
workInProgress: Fiber,

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ 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';
42+
4143
import checkPropTypes from 'shared/checkPropTypes';
4244
import {
4345
markComponentRenderStarted,
@@ -203,6 +205,7 @@ import {
203205
renderWithHooks,
204206
checkDidRenderIdHook,
205207
bailoutHooks,
208+
replaySuspendedComponentWithHooks,
206209
} from './ReactFiberHooks.old';
207210
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old';
208211
import {
@@ -1137,6 +1140,56 @@ function updateFunctionComponent(
11371140
return workInProgress.child;
11381141
}
11391142

1143+
export function replayFunctionComponent(
1144+
current: Fiber | null,
1145+
workInProgress: Fiber,
1146+
nextProps: any,
1147+
Component: any,
1148+
prevThenableState: ThenableState,
1149+
renderLanes: Lanes,
1150+
): Fiber | null {
1151+
// This function is used to replay a component that previously suspended,
1152+
// after its data resolves. It's a simplified version of
1153+
// updateFunctionComponent that reuses the hooks from the previous attempt.
1154+
1155+
let context;
1156+
if (!disableLegacyContext) {
1157+
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1158+
context = getMaskedContext(workInProgress, unmaskedContext);
1159+
}
1160+
1161+
prepareToReadContext(workInProgress, renderLanes);
1162+
if (enableSchedulingProfiler) {
1163+
markComponentRenderStarted(workInProgress);
1164+
}
1165+
const nextChildren = replaySuspendedComponentWithHooks(
1166+
current,
1167+
workInProgress,
1168+
Component,
1169+
nextProps,
1170+
context,
1171+
prevThenableState,
1172+
);
1173+
const hasId = checkDidRenderIdHook();
1174+
if (enableSchedulingProfiler) {
1175+
markComponentRenderStopped();
1176+
}
1177+
1178+
if (current !== null && !didReceiveUpdate) {
1179+
bailoutHooks(current, workInProgress, renderLanes);
1180+
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1181+
}
1182+
1183+
if (getIsHydrating() && hasId) {
1184+
pushMaterializedTreeId(workInProgress);
1185+
}
1186+
1187+
// React DevTools reads this flag.
1188+
workInProgress.flags |= PerformedWork;
1189+
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1190+
return workInProgress.child;
1191+
}
1192+
11401193
function updateClassComponent(
11411194
current: Fiber | null,
11421195
workInProgress: Fiber,

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,12 @@ export function renderWithHooks<Props, SecondArg>(
545545
}
546546
}
547547

548+
finishRenderingHooks(current, workInProgress);
549+
550+
return children;
551+
}
552+
553+
function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
548554
// We can assume the previous dispatcher is always this one, since we set it
549555
// at the beginning of the render phase and there's no re-entrance.
550556
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
@@ -638,7 +644,41 @@ export function renderWithHooks<Props, SecondArg>(
638644
}
639645
}
640646
}
647+
}
641648

649+
export function replaySuspendedComponentWithHooks<Props, SecondArg>(
650+
current: Fiber | null,
651+
workInProgress: Fiber,
652+
Component: (p: Props, arg: SecondArg) => any,
653+
props: Props,
654+
secondArg: SecondArg,
655+
prevThenableState: ThenableState | null,
656+
): any {
657+
// This function is used to replay a component that previously suspended,
658+
// after its data resolves.
659+
//
660+
// It's a simplified version of renderWithHooks, but it doesn't need to do
661+
// most of the set up work because they weren't reset when we suspended; they
662+
// only get reset when the component either completes (finishRenderingHooks)
663+
// or unwinds (resetHooksOnUnwind).
664+
if (__DEV__) {
665+
hookTypesDev =
666+
current !== null
667+
? ((current._debugHookTypes: any): Array<HookType>)
668+
: null;
669+
hookTypesUpdateIndexDev = -1;
670+
// Used for hot reloading:
671+
ignorePreviousDependencies =
672+
current !== null && current.type !== workInProgress.type;
673+
}
674+
const children = renderWithHooksAgain(
675+
workInProgress,
676+
Component,
677+
props,
678+
secondArg,
679+
prevThenableState,
680+
);
681+
finishRenderingHooks(current, workInProgress);
642682
return children;
643683
}
644684

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,12 @@ export function renderWithHooks<Props, SecondArg>(
545545
}
546546
}
547547

548+
finishRenderingHooks(current, workInProgress);
549+
550+
return children;
551+
}
552+
553+
function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
548554
// We can assume the previous dispatcher is always this one, since we set it
549555
// at the beginning of the render phase and there's no re-entrance.
550556
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
@@ -638,7 +644,41 @@ export function renderWithHooks<Props, SecondArg>(
638644
}
639645
}
640646
}
647+
}
641648

649+
export function replaySuspendedComponentWithHooks<Props, SecondArg>(
650+
current: Fiber | null,
651+
workInProgress: Fiber,
652+
Component: (p: Props, arg: SecondArg) => any,
653+
props: Props,
654+
secondArg: SecondArg,
655+
prevThenableState: ThenableState | null,
656+
): any {
657+
// This function is used to replay a component that previously suspended,
658+
// after its data resolves.
659+
//
660+
// It's a simplified version of renderWithHooks, but it doesn't need to do
661+
// most of the set up work because they weren't reset when we suspended; they
662+
// only get reset when the component either completes (finishRenderingHooks)
663+
// or unwinds (resetHooksOnUnwind).
664+
if (__DEV__) {
665+
hookTypesDev =
666+
current !== null
667+
? ((current._debugHookTypes: any): Array<HookType>)
668+
: null;
669+
hookTypesUpdateIndexDev = -1;
670+
// Used for hot reloading:
671+
ignorePreviousDependencies =
672+
current !== null && current.type !== workInProgress.type;
673+
}
674+
const children = renderWithHooksAgain(
675+
workInProgress,
676+
Component,
677+
props,
678+
secondArg,
679+
prevThenableState,
680+
);
681+
finishRenderingHooks(current, workInProgress);
642682
return children;
643683
}
644684

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

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ import {
178178
lanesToEventPriority,
179179
} from './ReactEventPriorities.new';
180180
import {requestCurrentTransition, NoTransition} from './ReactFiberTransition';
181-
import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new';
181+
import {
182+
beginWork as originalBeginWork,
183+
replayFunctionComponent,
184+
} from './ReactFiberBeginWork.new';
182185
import {completeWork} from './ReactFiberCompleteWork.new';
183186
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.new';
184187
import {
@@ -279,6 +282,7 @@ import {
279282
getSuspenseHandler,
280283
isBadSuspenseFallback,
281284
} from './ReactFiberSuspenseContext.new';
285+
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
282286

283287
const ceil = Math.ceil;
284288

@@ -2308,22 +2312,79 @@ function replaySuspendedUnitOfWork(
23082312
// This is a fork of performUnitOfWork specifcally for replaying a fiber that
23092313
// just suspended.
23102314
//
2311-
// Instead of unwinding the stack and potentially showing a fallback, unwind
2312-
// only the last stack frame, reset the fiber, and try rendering it again.
23132315
const current = unitOfWork.alternate;
2314-
resetSuspendedWorkLoopOnUnwind();
2315-
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2316-
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
2317-
23182316
setCurrentDebugFiberInDEV(unitOfWork);
23192317

23202318
let next;
2321-
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
2319+
setCurrentDebugFiberInDEV(unitOfWork);
2320+
const isProfilingMode =
2321+
enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode;
2322+
if (isProfilingMode) {
23222323
startProfilerTimer(unitOfWork);
2323-
next = beginWork(current, unitOfWork, renderLanes);
2324+
}
2325+
switch (unitOfWork.tag) {
2326+
case IndeterminateComponent: {
2327+
// Because it suspended with `use`, we can assume it's a
2328+
// function component.
2329+
unitOfWork.tag = FunctionComponent;
2330+
// Fallthrough to the next branch.
2331+
}
2332+
// eslint-disable-next-line no-fallthrough
2333+
case FunctionComponent:
2334+
case ForwardRef: {
2335+
// Resolve `defaultProps`. This logic is copied from `beginWork`.
2336+
// TODO: Consider moving this switch statement into that module. Also,
2337+
// could maybe use this as an opportunity to say `use` doesn't work with
2338+
// `defaultProps` :)
2339+
const Component = unitOfWork.type;
2340+
const unresolvedProps = unitOfWork.pendingProps;
2341+
const resolvedProps =
2342+
unitOfWork.elementType === Component
2343+
? unresolvedProps
2344+
: resolveDefaultProps(Component, unresolvedProps);
2345+
next = replayFunctionComponent(
2346+
current,
2347+
unitOfWork,
2348+
resolvedProps,
2349+
Component,
2350+
thenableState,
2351+
workInProgressRootRenderLanes,
2352+
);
2353+
break;
2354+
}
2355+
case SimpleMemoComponent: {
2356+
const Component = unitOfWork.type;
2357+
const nextProps = unitOfWork.pendingProps;
2358+
next = replayFunctionComponent(
2359+
current,
2360+
unitOfWork,
2361+
nextProps,
2362+
Component,
2363+
thenableState,
2364+
workInProgressRootRenderLanes,
2365+
);
2366+
break;
2367+
}
2368+
default: {
2369+
if (__DEV__) {
2370+
console.error(
2371+
'Unexpected type of work: %s, Currently only function ' +
2372+
'components are replayed after suspending. This is a bug in React.',
2373+
unitOfWork.tag,
2374+
);
2375+
}
2376+
resetSuspendedWorkLoopOnUnwind();
2377+
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2378+
unitOfWork = workInProgress = resetWorkInProgress(
2379+
unitOfWork,
2380+
renderLanes,
2381+
);
2382+
next = beginWork(current, unitOfWork, renderLanes);
2383+
break;
2384+
}
2385+
}
2386+
if (isProfilingMode) {
23242387
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2325-
} else {
2326-
next = beginWork(current, unitOfWork, renderLanes);
23272388
}
23282389

23292390
// The begin phase finished successfully without suspending. Reset the state

0 commit comments

Comments
 (0)