Skip to content

Commit 33e3d28

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 4387d75 commit 33e3d28

7 files changed

+411
-20
lines changed

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

+53
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 {
@@ -1159,6 +1162,56 @@ function updateFunctionComponent(
11591162
return workInProgress.child;
11601163
}
11611164

1165+
export function replayFunctionComponent(
1166+
current: Fiber | null,
1167+
workInProgress: Fiber,
1168+
nextProps: any,
1169+
Component: any,
1170+
prevThenableState: ThenableState,
1171+
renderLanes: Lanes,
1172+
): Fiber | null {
1173+
// This function is used to replay a component that previously suspended,
1174+
// after its data resolves. It's a simplified version of
1175+
// updateFunctionComponent that reuses the hooks from the previous attempt.
1176+
1177+
let context;
1178+
if (!disableLegacyContext) {
1179+
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1180+
context = getMaskedContext(workInProgress, unmaskedContext);
1181+
}
1182+
1183+
prepareToReadContext(workInProgress, renderLanes);
1184+
if (enableSchedulingProfiler) {
1185+
markComponentRenderStarted(workInProgress);
1186+
}
1187+
const nextChildren = replaySuspendedComponentWithHooks(
1188+
current,
1189+
workInProgress,
1190+
Component,
1191+
nextProps,
1192+
context,
1193+
prevThenableState,
1194+
);
1195+
const hasId = checkDidRenderIdHook();
1196+
if (enableSchedulingProfiler) {
1197+
markComponentRenderStopped();
1198+
}
1199+
1200+
if (current !== null && !didReceiveUpdate) {
1201+
bailoutHooks(current, workInProgress, renderLanes);
1202+
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1203+
}
1204+
1205+
if (getIsHydrating() && hasId) {
1206+
pushMaterializedTreeId(workInProgress);
1207+
}
1208+
1209+
// React DevTools reads this flag.
1210+
workInProgress.flags |= PerformedWork;
1211+
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1212+
return workInProgress.child;
1213+
}
1214+
11621215
function updateClassComponent(
11631216
current: Fiber | null,
11641217
workInProgress: Fiber,

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

+53
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 {
@@ -1159,6 +1162,56 @@ function updateFunctionComponent(
11591162
return workInProgress.child;
11601163
}
11611164

1165+
export function replayFunctionComponent(
1166+
current: Fiber | null,
1167+
workInProgress: Fiber,
1168+
nextProps: any,
1169+
Component: any,
1170+
prevThenableState: ThenableState,
1171+
renderLanes: Lanes,
1172+
): Fiber | null {
1173+
// This function is used to replay a component that previously suspended,
1174+
// after its data resolves. It's a simplified version of
1175+
// updateFunctionComponent that reuses the hooks from the previous attempt.
1176+
1177+
let context;
1178+
if (!disableLegacyContext) {
1179+
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
1180+
context = getMaskedContext(workInProgress, unmaskedContext);
1181+
}
1182+
1183+
prepareToReadContext(workInProgress, renderLanes);
1184+
if (enableSchedulingProfiler) {
1185+
markComponentRenderStarted(workInProgress);
1186+
}
1187+
const nextChildren = replaySuspendedComponentWithHooks(
1188+
current,
1189+
workInProgress,
1190+
Component,
1191+
nextProps,
1192+
context,
1193+
prevThenableState,
1194+
);
1195+
const hasId = checkDidRenderIdHook();
1196+
if (enableSchedulingProfiler) {
1197+
markComponentRenderStopped();
1198+
}
1199+
1200+
if (current !== null && !didReceiveUpdate) {
1201+
bailoutHooks(current, workInProgress, renderLanes);
1202+
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1203+
}
1204+
1205+
if (getIsHydrating() && hasId) {
1206+
pushMaterializedTreeId(workInProgress);
1207+
}
1208+
1209+
// React DevTools reads this flag.
1210+
workInProgress.flags |= PerformedWork;
1211+
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1212+
return workInProgress.child;
1213+
}
1214+
11621215
function updateClassComponent(
11631216
current: Fiber | null,
11641217
workInProgress: Fiber,

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

+40
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

+40
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

+69-10
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ import {requestCurrentTransition, NoTransition} from './ReactFiberTransition';
181181
import {
182182
SelectiveHydrationException,
183183
beginWork as originalBeginWork,
184+
replayFunctionComponent,
184185
} from './ReactFiberBeginWork.new';
185186
import {completeWork} from './ReactFiberCompleteWork.new';
186187
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.new';
@@ -282,6 +283,7 @@ import {
282283
getSuspenseHandler,
283284
isBadSuspenseFallback,
284285
} from './ReactFiberSuspenseContext.new';
286+
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
285287

286288
const ceil = Math.ceil;
287289

@@ -2353,22 +2355,79 @@ function replaySuspendedUnitOfWork(
23532355
// This is a fork of performUnitOfWork specifcally for replaying a fiber that
23542356
// just suspended.
23552357
//
2356-
// Instead of unwinding the stack and potentially showing a fallback, unwind
2357-
// only the last stack frame, reset the fiber, and try rendering it again.
23582358
const current = unitOfWork.alternate;
2359-
resetSuspendedWorkLoopOnUnwind();
2360-
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2361-
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
2362-
23632359
setCurrentDebugFiberInDEV(unitOfWork);
23642360

23652361
let next;
2366-
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
2362+
setCurrentDebugFiberInDEV(unitOfWork);
2363+
const isProfilingMode =
2364+
enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode;
2365+
if (isProfilingMode) {
23672366
startProfilerTimer(unitOfWork);
2368-
next = beginWork(current, unitOfWork, renderLanes);
2367+
}
2368+
switch (unitOfWork.tag) {
2369+
case IndeterminateComponent: {
2370+
// Because it suspended with `use`, we can assume it's a
2371+
// function component.
2372+
unitOfWork.tag = FunctionComponent;
2373+
// Fallthrough to the next branch.
2374+
}
2375+
// eslint-disable-next-line no-fallthrough
2376+
case FunctionComponent:
2377+
case ForwardRef: {
2378+
// Resolve `defaultProps`. This logic is copied from `beginWork`.
2379+
// TODO: Consider moving this switch statement into that module. Also,
2380+
// could maybe use this as an opportunity to say `use` doesn't work with
2381+
// `defaultProps` :)
2382+
const Component = unitOfWork.type;
2383+
const unresolvedProps = unitOfWork.pendingProps;
2384+
const resolvedProps =
2385+
unitOfWork.elementType === Component
2386+
? unresolvedProps
2387+
: resolveDefaultProps(Component, unresolvedProps);
2388+
next = replayFunctionComponent(
2389+
current,
2390+
unitOfWork,
2391+
resolvedProps,
2392+
Component,
2393+
thenableState,
2394+
workInProgressRootRenderLanes,
2395+
);
2396+
break;
2397+
}
2398+
case SimpleMemoComponent: {
2399+
const Component = unitOfWork.type;
2400+
const nextProps = unitOfWork.pendingProps;
2401+
next = replayFunctionComponent(
2402+
current,
2403+
unitOfWork,
2404+
nextProps,
2405+
Component,
2406+
thenableState,
2407+
workInProgressRootRenderLanes,
2408+
);
2409+
break;
2410+
}
2411+
default: {
2412+
if (__DEV__) {
2413+
console.error(
2414+
'Unexpected type of work: %s, Currently only function ' +
2415+
'components are replayed after suspending. This is a bug in React.',
2416+
unitOfWork.tag,
2417+
);
2418+
}
2419+
resetSuspendedWorkLoopOnUnwind();
2420+
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2421+
unitOfWork = workInProgress = resetWorkInProgress(
2422+
unitOfWork,
2423+
renderLanes,
2424+
);
2425+
next = beginWork(current, unitOfWork, renderLanes);
2426+
break;
2427+
}
2428+
}
2429+
if (isProfilingMode) {
23692430
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
2370-
} else {
2371-
next = beginWork(current, unitOfWork, renderLanes);
23722431
}
23732432

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

0 commit comments

Comments
 (0)