mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Implement Partial Hydration for Activity (#32863)
Stacked on #32862 and #32842. This means that Activity boundaries now act as boundaries which can have their effects mounted independently. Just like Suspense boundaries, we hydrate the outer content first and then start hydrating the content in an Offscreen lane. Flowing props or interacting with the content increases the priority just like Suspense boundaries. This skips emitting even the comments for `<Activity mode="hidden">` so we don't hydrate those. Instead those are deferred to a later client render. The implementation are just forked copies of the SuspenseComponent branches and then carefully going through each line and tweaking it. The main interesting bit is that, unlike Suspense, Activity boundaries don't have fallbacks so all those branches where you might commit a suspended tree disappears. Instead, if something suspends while hydration, we can just leave the dehydrated content in place. However, if something does suspend during client rendering then it should bubble up to the parent. Therefore, we have to be careful to only pushSuspenseHandler when hydrating. That's really the main difference. This just uses the existing basic Activity tests but I've started work on port all of the applicable Suspense tests in SelectiveHydration-test and PartialHydration-test to Activity versions.
This commit is contained in:
committed by
GitHub
parent
65ec1c18fb
commit
2a3b50de29
@@ -3746,11 +3746,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
<span>
|
||||
Visible
|
||||
</span>
|
||||
<!--&-->
|
||||
<!--/&-->
|
||||
<!--$-->
|
||||
<!--&-->
|
||||
<!--/&-->
|
||||
<!--/$-->
|
||||
</div>
|
||||
`);
|
||||
@@ -3766,6 +3762,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
// Passive effects.
|
||||
await waitForPaint([]);
|
||||
}
|
||||
|
||||
// Subsequently, the hidden child is prerendered on the client
|
||||
// along with hydrating the Suspense boundary outside the Activity.
|
||||
await waitForPaint(['HiddenChild']);
|
||||
@@ -3774,11 +3771,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
<span>
|
||||
Visible
|
||||
</span>
|
||||
<!--&-->
|
||||
<!--/&-->
|
||||
<!--$-->
|
||||
<!--&-->
|
||||
<!--/&-->
|
||||
<!--/$-->
|
||||
<span
|
||||
style="display: none;"
|
||||
@@ -3796,11 +3789,7 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
<span>
|
||||
Visible
|
||||
</span>
|
||||
<!--&-->
|
||||
<!--/&-->
|
||||
<!--$-->
|
||||
<!--&-->
|
||||
<!--/&-->
|
||||
<!--/$-->
|
||||
<span
|
||||
style="display: none;"
|
||||
|
||||
+3014
File diff suppressed because it is too large
Load Diff
+1609
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -20,7 +20,7 @@ import type {RootTag} from './ReactRootTags';
|
||||
import type {WorkTag} from './ReactWorkTags';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
import type {Lanes} from './ReactFiberLane';
|
||||
import type {SuspenseInstance} from './ReactFiberConfig';
|
||||
import type {ActivityInstance, SuspenseInstance} from './ReactFiberConfig';
|
||||
import type {
|
||||
LegacyHiddenProps,
|
||||
OffscreenProps,
|
||||
@@ -951,7 +951,7 @@ export function createFiberFromText(
|
||||
}
|
||||
|
||||
export function createFiberFromDehydratedFragment(
|
||||
dehydratedNode: SuspenseInstance,
|
||||
dehydratedNode: SuspenseInstance | ActivityInstance,
|
||||
): Fiber {
|
||||
const fiber = createFiber(DehydratedFragment, null, null, NoMode);
|
||||
fiber.stateNode = dehydratedNode;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ActivityInstance} from './ReactFiberConfig';
|
||||
import type {CapturedValue} from './ReactCapturedValue';
|
||||
import type {Lane} from './ReactFiberLane';
|
||||
import type {TreeContext} from './ReactFiberTreeContext';
|
||||
|
||||
// A non-null ActivityState represents a dehydrated Activity boundary.
|
||||
export type ActivityState = {
|
||||
dehydrated: ActivityInstance,
|
||||
treeContext: null | TreeContext,
|
||||
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
|
||||
// OffscreenLane is the default for dehydrated boundaries.
|
||||
// NoLane is the default for normal boundaries, which turns into "normal" pri.
|
||||
retryLane: Lane,
|
||||
// Stashed Errors that happened while attempting to hydrate this boundary.
|
||||
hydrationErrors: Array<CapturedValue<mixed>> | null,
|
||||
};
|
||||
+277
-29
@@ -22,6 +22,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
import type {Lanes, Lane} from './ReactFiberLane';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {
|
||||
SuspenseState,
|
||||
SuspenseListRenderState,
|
||||
@@ -185,7 +186,7 @@ import {
|
||||
createHoistableInstance,
|
||||
HostTransitionContext,
|
||||
} from './ReactFiberConfig';
|
||||
import type {SuspenseInstance} from './ReactFiberConfig';
|
||||
import type {ActivityInstance, SuspenseInstance} from './ReactFiberConfig';
|
||||
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
|
||||
import {
|
||||
pushHostContext,
|
||||
@@ -201,8 +202,10 @@ import {
|
||||
setShallowSuspenseListContext,
|
||||
pushPrimaryTreeSuspenseHandler,
|
||||
pushFallbackTreeSuspenseHandler,
|
||||
pushDehydratedActivitySuspenseHandler,
|
||||
pushOffscreenSuspenseHandler,
|
||||
reuseSuspenseHandlerOnStack,
|
||||
popSuspenseHandler,
|
||||
} from './ReactFiberSuspenseContext';
|
||||
import {
|
||||
pushHiddenContext,
|
||||
@@ -239,6 +242,7 @@ import {
|
||||
import {
|
||||
getIsHydrating,
|
||||
enterHydrationState,
|
||||
reenterHydrationStateFromDehydratedActivityInstance,
|
||||
reenterHydrationStateFromDehydratedSuspenseInstance,
|
||||
resetHydrationState,
|
||||
claimHydratableSingleton,
|
||||
@@ -713,14 +717,7 @@ function updateOffscreenComponent(
|
||||
}
|
||||
reuseHiddenContextOnStack(workInProgress);
|
||||
pushOffscreenSuspenseHandler(workInProgress);
|
||||
} else if (
|
||||
!includesSomeLane(renderLanes, (OffscreenLane: Lane)) ||
|
||||
// SSR doesn't render hidden content (except legacy hidden) so it shouldn't hydrate,
|
||||
// even at offscreen lane. Defer to a client rendered offscreen lane.
|
||||
(getIsHydrating() &&
|
||||
(!enableLegacyHidden ||
|
||||
nextProps.mode !== 'unstable-defer-without-hiding'))
|
||||
) {
|
||||
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
|
||||
// We're hidden, and we're not rendering at Offscreen. We will bail out
|
||||
// and resume this tree later.
|
||||
|
||||
@@ -875,12 +872,11 @@ function updateLegacyHiddenComponent(
|
||||
);
|
||||
}
|
||||
|
||||
function updateActivityComponent(
|
||||
current: null | Fiber,
|
||||
function mountActivityChildren(
|
||||
workInProgress: Fiber,
|
||||
nextProps: ActivityProps,
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
const nextProps: ActivityProps = workInProgress.pendingProps;
|
||||
if (__DEV__) {
|
||||
const hiddenProp = (nextProps: any).hidden;
|
||||
if (hiddenProp !== undefined) {
|
||||
@@ -904,25 +900,268 @@ function updateActivityComponent(
|
||||
mode: nextMode,
|
||||
children: nextChildren,
|
||||
};
|
||||
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
|
||||
offscreenChildProps,
|
||||
mode,
|
||||
renderLanes,
|
||||
);
|
||||
primaryChildFragment.ref = workInProgress.ref;
|
||||
workInProgress.child = primaryChildFragment;
|
||||
primaryChildFragment.return = workInProgress;
|
||||
return primaryChildFragment;
|
||||
}
|
||||
|
||||
if (current === null) {
|
||||
if (getIsHydrating()) {
|
||||
claimNextHydratableActivityInstance(workInProgress);
|
||||
function retryActivityComponentWithoutHydrating(
|
||||
current: Fiber,
|
||||
workInProgress: Fiber,
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
// Falling back to client rendering. Because this has performance
|
||||
// implications, it's considered a recoverable error, even though the user
|
||||
// likely won't observe anything wrong with the UI.
|
||||
|
||||
// This will add the old fiber to the deletion list
|
||||
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
|
||||
|
||||
// We're now not suspended nor dehydrated.
|
||||
const nextProps: ActivityProps = workInProgress.pendingProps;
|
||||
const primaryChildFragment = mountActivityChildren(
|
||||
workInProgress,
|
||||
nextProps,
|
||||
renderLanes,
|
||||
);
|
||||
// Needs a placement effect because the parent (the Activity boundary) already
|
||||
// mounted but this is a new fiber.
|
||||
primaryChildFragment.flags |= Placement;
|
||||
|
||||
// If we're not going to hydrate we can't leave it dehydrated if something
|
||||
// suspends. In that case we want that to bubble to the nearest parent boundary
|
||||
// so we need to pop our own handler that we just pushed.
|
||||
popSuspenseHandler(workInProgress);
|
||||
|
||||
workInProgress.memoizedState = null;
|
||||
|
||||
return primaryChildFragment;
|
||||
}
|
||||
|
||||
function mountDehydratedActivityComponent(
|
||||
workInProgress: Fiber,
|
||||
activityInstance: ActivityInstance,
|
||||
renderLanes: Lanes,
|
||||
): null | Fiber {
|
||||
// During the first pass, we'll bail out and not drill into the children.
|
||||
// Instead, we'll leave the content in place and try to hydrate it later.
|
||||
// We'll continue hydrating the rest at offscreen priority since we'll already
|
||||
// be showing the right content coming from the server, it is no rush.
|
||||
workInProgress.lanes = laneToLanes(OffscreenLane);
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateDehydratedActivityComponent(
|
||||
current: Fiber,
|
||||
workInProgress: Fiber,
|
||||
didSuspend: boolean,
|
||||
nextProps: ActivityProps,
|
||||
activityInstance: ActivityInstance,
|
||||
activityState: ActivityState,
|
||||
renderLanes: Lanes,
|
||||
): null | Fiber {
|
||||
// We'll handle suspending since if something suspends we can just leave
|
||||
// it dehydrated. We push early and then pop if we enter non-dehydrated attempts.
|
||||
pushDehydratedActivitySuspenseHandler(workInProgress);
|
||||
if (!didSuspend) {
|
||||
// This is the first render pass. Attempt to hydrate.
|
||||
|
||||
// We should never be hydrating at this point because it is the first pass,
|
||||
// but after we've already committed once.
|
||||
warnIfHydrating();
|
||||
|
||||
if (
|
||||
// TODO: Factoring is a little weird, since we check this right below, too.
|
||||
!didReceiveUpdate
|
||||
) {
|
||||
// We need to check if any children have context before we decide to bail
|
||||
// out, so propagate the changes now.
|
||||
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
|
||||
}
|
||||
|
||||
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
|
||||
offscreenChildProps,
|
||||
mode,
|
||||
renderLanes,
|
||||
);
|
||||
primaryChildFragment.ref = workInProgress.ref;
|
||||
workInProgress.child = primaryChildFragment;
|
||||
primaryChildFragment.return = workInProgress;
|
||||
// We use lanes to indicate that a child might depend on context, so if
|
||||
// any context has changed, we need to treat is as if the input might have changed.
|
||||
const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
|
||||
if (didReceiveUpdate || hasContextChanged) {
|
||||
// This boundary has changed since the first render. This means that we are now unable to
|
||||
// hydrate it. We might still be able to hydrate it using a higher priority lane.
|
||||
const root = getWorkInProgressRoot();
|
||||
if (root !== null) {
|
||||
const attemptHydrationAtLane = getBumpedLaneForHydration(
|
||||
root,
|
||||
renderLanes,
|
||||
);
|
||||
if (
|
||||
attemptHydrationAtLane !== NoLane &&
|
||||
attemptHydrationAtLane !== activityState.retryLane
|
||||
) {
|
||||
// Intentionally mutating since this render will get interrupted. This
|
||||
// is one of the very rare times where we mutate the current tree
|
||||
// during the render phase.
|
||||
activityState.retryLane = attemptHydrationAtLane;
|
||||
enqueueConcurrentRenderForLane(current, attemptHydrationAtLane);
|
||||
scheduleUpdateOnFiber(root, current, attemptHydrationAtLane);
|
||||
|
||||
return primaryChildFragment;
|
||||
// Throw a special object that signals to the work loop that it should
|
||||
// interrupt the current render.
|
||||
//
|
||||
// Because we're inside a React-only execution stack, we don't
|
||||
// strictly need to throw here — we could instead modify some internal
|
||||
// work loop state. But using an exception means we don't need to
|
||||
// check for this case on every iteration of the work loop. So doing
|
||||
// it this way moves the check out of the fast path.
|
||||
throw SelectiveHydrationException;
|
||||
} else {
|
||||
// We have already tried to ping at a higher priority than we're rendering with
|
||||
// so if we got here, we must have failed to hydrate at those levels. We must
|
||||
// now give up. Instead, we're going to delete the whole subtree and instead inject
|
||||
// a new real Activity boundary to take its place. This might suspend for a while
|
||||
// and if it does we might still have an opportunity to hydrate before this pass
|
||||
// commits.
|
||||
}
|
||||
}
|
||||
|
||||
// If we did not selectively hydrate, we'll continue rendering without
|
||||
// hydrating. Mark this tree as suspended to prevent it from committing
|
||||
// outside a transition.
|
||||
//
|
||||
// This path should only happen if the hydration lane already suspended.
|
||||
renderDidSuspendDelayIfPossible();
|
||||
return retryActivityComponentWithoutHydrating(
|
||||
current,
|
||||
workInProgress,
|
||||
renderLanes,
|
||||
);
|
||||
} else {
|
||||
// This is the first attempt.
|
||||
|
||||
reenterHydrationStateFromDehydratedActivityInstance(
|
||||
workInProgress,
|
||||
activityInstance,
|
||||
activityState.treeContext,
|
||||
);
|
||||
|
||||
const primaryChildFragment = mountActivityChildren(
|
||||
workInProgress,
|
||||
nextProps,
|
||||
renderLanes,
|
||||
);
|
||||
// Mark the children as hydrating. This is a fast path to know whether this
|
||||
// tree is part of a hydrating tree. This is used to determine if a child
|
||||
// node has fully mounted yet, and for scheduling event replaying.
|
||||
// Conceptually this is similar to Placement in that a new subtree is
|
||||
// inserted into the React tree here. It just happens to not need DOM
|
||||
// mutations because it already exists.
|
||||
primaryChildFragment.flags |= Hydrating;
|
||||
return primaryChildFragment;
|
||||
}
|
||||
} else {
|
||||
// This is the second render pass. We already attempted to hydrated, but
|
||||
// something either suspended or errored.
|
||||
|
||||
if (workInProgress.flags & ForceClientRender) {
|
||||
// Something errored during hydration. Try again without hydrating.
|
||||
// The error should've already been logged in throwException.
|
||||
workInProgress.flags &= ~ForceClientRender;
|
||||
return retryActivityComponentWithoutHydrating(
|
||||
current,
|
||||
workInProgress,
|
||||
renderLanes,
|
||||
);
|
||||
} else if ((workInProgress.memoizedState: null | ActivityState) !== null) {
|
||||
// Something suspended and we should still be in dehydrated mode.
|
||||
// Leave the existing child in place.
|
||||
|
||||
workInProgress.child = current.child;
|
||||
// The dehydrated completion pass expects this flag to be there
|
||||
// but the normal offscreen pass doesn't.
|
||||
workInProgress.flags |= DidCapture;
|
||||
return null;
|
||||
} else {
|
||||
// We called retryActivityComponentWithoutHydrating and tried client rendering
|
||||
// but now we suspended again. We should never arrive here because we should
|
||||
// not have pushed a suspense handler during that second pass and it should
|
||||
// instead have suspended above.
|
||||
throw new Error(
|
||||
'Client rendering an Activity suspended it again. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateActivityComponent(
|
||||
current: null | Fiber,
|
||||
workInProgress: Fiber,
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
const nextProps: ActivityProps = workInProgress.pendingProps;
|
||||
|
||||
// Check if the first pass suspended.
|
||||
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
|
||||
workInProgress.flags &= ~DidCapture;
|
||||
|
||||
if (current === null) {
|
||||
// Initial mount
|
||||
|
||||
// Special path for hydration
|
||||
// If we're currently hydrating, try to hydrate this boundary.
|
||||
// Hidden Activity boundaries are not emitted on the server.
|
||||
if (getIsHydrating()) {
|
||||
if (nextProps.mode === 'hidden') {
|
||||
// SSR doesn't render hidden Activity so it shouldn't hydrate,
|
||||
// even at offscreen lane. Defer to a client rendered offscreen lane.
|
||||
mountActivityChildren(workInProgress, nextProps, renderLanes);
|
||||
workInProgress.lanes = laneToLanes(OffscreenLane);
|
||||
return null;
|
||||
} else {
|
||||
// We must push the suspense handler context *before* attempting to
|
||||
// hydrate, to avoid a mismatch in case it errors.
|
||||
pushDehydratedActivitySuspenseHandler(workInProgress);
|
||||
const dehydrated: ActivityInstance =
|
||||
claimNextHydratableActivityInstance(workInProgress);
|
||||
return mountDehydratedActivityComponent(
|
||||
workInProgress,
|
||||
dehydrated,
|
||||
renderLanes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return mountActivityChildren(workInProgress, nextProps, renderLanes);
|
||||
} else {
|
||||
// This is an update.
|
||||
|
||||
// Special path for hydration
|
||||
const prevState: null | ActivityState = current.memoizedState;
|
||||
|
||||
if (prevState !== null) {
|
||||
const dehydrated = prevState.dehydrated;
|
||||
return updateDehydratedActivityComponent(
|
||||
current,
|
||||
workInProgress,
|
||||
didSuspend,
|
||||
nextProps,
|
||||
dehydrated,
|
||||
prevState,
|
||||
renderLanes,
|
||||
);
|
||||
}
|
||||
|
||||
const currentChild: Fiber = (current.child: any);
|
||||
|
||||
const nextChildren = nextProps.children;
|
||||
const nextMode = nextProps.mode;
|
||||
const offscreenChildProps: OffscreenProps = {
|
||||
mode: nextMode,
|
||||
children: nextChildren,
|
||||
};
|
||||
|
||||
const primaryChildFragment = updateWorkInProgressOffscreenFiber(
|
||||
currentChild,
|
||||
offscreenChildProps,
|
||||
@@ -2801,11 +3040,6 @@ function updateDehydratedSuspenseComponent(
|
||||
// outside a transition.
|
||||
//
|
||||
// This path should only happen if the hydration lane already suspended.
|
||||
// Currently, it also happens during sync updates because there is no
|
||||
// hydration lane for sync updates.
|
||||
// TODO: We should ideally have a sync hydration lane that we can apply to do
|
||||
// a pass where we hydrate this subtree in place using the previous Context and then
|
||||
// reapply the update afterwards.
|
||||
if (isSuspenseInstancePending(suspenseInstance)) {
|
||||
// This is a dehydrated suspense instance. We don't need to suspend
|
||||
// because we're already showing a fallback.
|
||||
@@ -3705,6 +3939,20 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ActivityComponent: {
|
||||
const state: ActivityState | null = workInProgress.memoizedState;
|
||||
if (state !== null) {
|
||||
// We're dehydrated so we're not going to render the children. This is just
|
||||
// to maintain push/pop symmetry.
|
||||
// We know that this component will suspend again because if it has
|
||||
// been unsuspended it has committed as a hydrated Activity component.
|
||||
// If it needs to be retried, it should have work scheduled on it.
|
||||
workInProgress.flags |= DidCapture;
|
||||
pushDehydratedActivitySuspenseHandler(workInProgress);
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
const state: SuspenseState | null = workInProgress.memoizedState;
|
||||
if (state !== null) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type {
|
||||
Instance,
|
||||
TextInstance,
|
||||
ActivityInstance,
|
||||
SuspenseInstance,
|
||||
Container,
|
||||
ChildSet,
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
unhideInstance,
|
||||
unhideTextInstance,
|
||||
commitHydratedContainer,
|
||||
commitHydratedActivityInstance,
|
||||
commitHydratedSuspenseInstance,
|
||||
removeChildFromContainer,
|
||||
removeChild,
|
||||
@@ -682,6 +684,25 @@ export function commitHostHydratedContainer(
|
||||
}
|
||||
}
|
||||
|
||||
export function commitHostHydratedActivity(
|
||||
activityInstance: ActivityInstance,
|
||||
finishedWork: Fiber,
|
||||
) {
|
||||
try {
|
||||
if (__DEV__) {
|
||||
runWithFiberInDEV(
|
||||
finishedWork,
|
||||
commitHydratedActivityInstance,
|
||||
activityInstance,
|
||||
);
|
||||
} else {
|
||||
commitHydratedActivityInstance(activityInstance);
|
||||
}
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
||||
}
|
||||
}
|
||||
|
||||
export function commitHostHydratedSuspense(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
finishedWork: Fiber,
|
||||
|
||||
+138
-2
@@ -10,6 +10,7 @@
|
||||
import type {
|
||||
Instance,
|
||||
TextInstance,
|
||||
ActivityInstance,
|
||||
SuspenseInstance,
|
||||
Container,
|
||||
HoistableRoot,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
includesOnlySuspenseyCommitEligibleLanes,
|
||||
includesOnlyViewTransitionEligibleLanes,
|
||||
} from './ReactFiberLane';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {SuspenseState, RetryQueue} from './ReactFiberSuspenseComponent';
|
||||
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
|
||||
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
|
||||
@@ -69,6 +71,7 @@ import {
|
||||
HostText,
|
||||
HostPortal,
|
||||
Profiler,
|
||||
ActivityComponent,
|
||||
SuspenseComponent,
|
||||
DehydratedFragment,
|
||||
IncompleteClassComponent,
|
||||
@@ -234,6 +237,7 @@ import {
|
||||
commitHostRootContainerChildren,
|
||||
commitHostPortalContainerChildren,
|
||||
commitHostHydratedContainer,
|
||||
commitHostHydratedActivity,
|
||||
commitHostHydratedSuspense,
|
||||
commitHostRemoveChildFromContainer,
|
||||
commitHostRemoveChild,
|
||||
@@ -294,7 +298,11 @@ let viewTransitionContextChanged: boolean = false;
|
||||
let rootViewTransitionAffected: boolean = false;
|
||||
|
||||
function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean {
|
||||
if (finishedWork.tag === SuspenseComponent) {
|
||||
if (finishedWork.tag === ActivityComponent) {
|
||||
const prevState: ActivityState | null = current.memoizedState;
|
||||
const nextState: ActivityState | null = finishedWork.memoizedState;
|
||||
return prevState !== null && nextState === null;
|
||||
} else if (finishedWork.tag === SuspenseComponent) {
|
||||
const prevState: SuspenseState | null = current.memoizedState;
|
||||
const nextState: SuspenseState | null = finishedWork.memoizedState;
|
||||
return (
|
||||
@@ -454,6 +462,7 @@ function commitBeforeMutationEffectsOnFiber(
|
||||
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
|
||||
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
|
||||
// TODO: Move this out of the hot path using a dedicated effect tag.
|
||||
// TODO: This should consider Offscreen in general and not just SuspenseComponent.
|
||||
if (
|
||||
finishedWork.tag === SuspenseComponent &&
|
||||
isSuspenseBoundaryBeingHidden(current, finishedWork) &&
|
||||
@@ -700,6 +709,17 @@ function commitLayoutEffectOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActivityComponent: {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
finishedWork,
|
||||
committedLanes,
|
||||
);
|
||||
if (flags & Update) {
|
||||
commitActivityHydrationCallbacks(finishedRoot, finishedWork);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
@@ -1508,7 +1528,9 @@ function commitDeletionEffectsOnFiber(
|
||||
try {
|
||||
const onDeleted = hydrationCallbacks.onDeleted;
|
||||
if (onDeleted) {
|
||||
onDeleted((deletedFiber.stateNode: SuspenseInstance));
|
||||
onDeleted(
|
||||
(deletedFiber.stateNode: SuspenseInstance | ActivityInstance),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(
|
||||
@@ -1744,6 +1766,40 @@ function commitSuspenseCallback(finishedWork: Fiber) {
|
||||
}
|
||||
}
|
||||
|
||||
function commitActivityHydrationCallbacks(
|
||||
finishedRoot: FiberRoot,
|
||||
finishedWork: Fiber,
|
||||
) {
|
||||
if (!supportsHydration) {
|
||||
return;
|
||||
}
|
||||
const newState: ActivityState | null = finishedWork.memoizedState;
|
||||
if (newState === null) {
|
||||
const current = finishedWork.alternate;
|
||||
if (current !== null) {
|
||||
const prevState: ActivityState | null = current.memoizedState;
|
||||
if (prevState !== null) {
|
||||
const activityInstance = prevState.dehydrated;
|
||||
commitHostHydratedActivity(activityInstance, finishedWork);
|
||||
if (enableSuspenseCallback) {
|
||||
try {
|
||||
// TODO: Delete this feature. It's not properly covered by DEV features.
|
||||
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
|
||||
if (hydrationCallbacks !== null) {
|
||||
const onHydrated = hydrationCallbacks.onHydrated;
|
||||
if (onHydrated) {
|
||||
onHydrated(activityInstance);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function commitSuspenseHydrationCallbacks(
|
||||
finishedRoot: FiberRoot,
|
||||
finishedWork: Fiber,
|
||||
@@ -1784,6 +1840,7 @@ function getRetryCache(finishedWork: Fiber) {
|
||||
// TODO: Unify the interface for the retry cache so we don't have to switch
|
||||
// on the tag like this.
|
||||
switch (finishedWork.tag) {
|
||||
case ActivityComponent:
|
||||
case SuspenseComponent:
|
||||
case SuspenseListComponent: {
|
||||
let retryCache = finishedWork.stateNode;
|
||||
@@ -2239,6 +2296,18 @@ function commitMutationEffectsOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActivityComponent: {
|
||||
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
|
||||
commitReconciliationEffects(finishedWork, lanes);
|
||||
if (flags & Update) {
|
||||
const retryQueue: RetryQueue | null = (finishedWork.updateQueue: any);
|
||||
if (retryQueue !== null) {
|
||||
finishedWork.updateQueue = null;
|
||||
attachSuspenseRetryListeners(finishedWork, retryQueue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
|
||||
commitReconciliationEffects(finishedWork, lanes);
|
||||
@@ -3020,6 +3089,19 @@ export function reappearLayoutEffects(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActivityComponent: {
|
||||
recursivelyTraverseReappearLayoutEffects(
|
||||
finishedRoot,
|
||||
finishedWork,
|
||||
includeWorkInProgressEffects,
|
||||
);
|
||||
|
||||
if (includeWorkInProgressEffects && flags & Update) {
|
||||
// TODO: Delete this feature.
|
||||
commitActivityHydrationCallbacks(finishedRoot, finishedWork);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
recursivelyTraverseReappearLayoutEffects(
|
||||
finishedRoot,
|
||||
@@ -3581,6 +3663,60 @@ function commitPassiveMountOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActivityComponent: {
|
||||
const wasInHydratedSubtree = inHydratedSubtree;
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
const prevState: ActivityState | null =
|
||||
finishedWork.alternate !== null
|
||||
? finishedWork.alternate.memoizedState
|
||||
: null;
|
||||
const nextState: ActivityState | null = finishedWork.memoizedState;
|
||||
if (prevState !== null && nextState === null) {
|
||||
// This was dehydrated but is no longer dehydrated. We may have now either hydrated it
|
||||
// or client rendered it.
|
||||
const deletions = finishedWork.deletions;
|
||||
if (
|
||||
deletions !== null &&
|
||||
deletions.length > 0 &&
|
||||
deletions[0].tag === DehydratedFragment
|
||||
) {
|
||||
// This was an abandoned hydration that deleted the dehydrated fragment. That means we
|
||||
// are not hydrating this Suspense boundary.
|
||||
inHydratedSubtree = false;
|
||||
const hydrationErrors = prevState.hydrationErrors;
|
||||
// If there were no hydration errors, that suggests that this was an intentional client
|
||||
// rendered boundary. Such as postpone.
|
||||
if (hydrationErrors !== null) {
|
||||
const startTime: number = (finishedWork.actualStartTime: any);
|
||||
logComponentErrored(
|
||||
finishedWork,
|
||||
startTime,
|
||||
endTime,
|
||||
hydrationErrors,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If any children committed they were hydrated.
|
||||
inHydratedSubtree = true;
|
||||
}
|
||||
} else {
|
||||
inHydratedSubtree = false;
|
||||
}
|
||||
}
|
||||
|
||||
recursivelyTraversePassiveMountEffects(
|
||||
finishedRoot,
|
||||
finishedWork,
|
||||
committedLanes,
|
||||
committedTransitions,
|
||||
endTime,
|
||||
);
|
||||
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
inHydratedSubtree = wasInHydratedSubtree;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
const wasInHydratedSubtree = inHydratedSubtree;
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
|
||||
+119
-6
@@ -19,6 +19,7 @@ import type {
|
||||
ChildSet,
|
||||
Resource,
|
||||
} from './ReactFiberConfig';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {
|
||||
SuspenseState,
|
||||
SuspenseListRenderState,
|
||||
@@ -154,6 +155,7 @@ import {popProvider} from './ReactFiberNewContext';
|
||||
import {
|
||||
prepareToHydrateHostInstance,
|
||||
prepareToHydrateHostTextInstance,
|
||||
prepareToHydrateHostActivityInstance,
|
||||
prepareToHydrateHostSuspenseInstance,
|
||||
popHydrationState,
|
||||
resetHydrationState,
|
||||
@@ -897,6 +899,88 @@ function bubbleProperties(completedWork: Fiber) {
|
||||
return didBailout;
|
||||
}
|
||||
|
||||
function completeDehydratedActivityBoundary(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
nextState: ActivityState | null,
|
||||
): boolean {
|
||||
const wasHydrated = popHydrationState(workInProgress);
|
||||
|
||||
if (nextState !== null) {
|
||||
// We might be inside a hydration state the first time we're picking up this
|
||||
// Activity boundary, and also after we've reentered it for further hydration.
|
||||
if (current === null) {
|
||||
if (!wasHydrated) {
|
||||
throw new Error(
|
||||
'A dehydrated suspense component was completed without a hydrated node. ' +
|
||||
'This is probably a bug in React.',
|
||||
);
|
||||
}
|
||||
prepareToHydrateHostActivityInstance(workInProgress);
|
||||
bubbleProperties(workInProgress);
|
||||
if (enableProfilerTimer) {
|
||||
if ((workInProgress.mode & ProfileMode) !== NoMode) {
|
||||
const isTimedOutSuspense = nextState !== null;
|
||||
if (isTimedOutSuspense) {
|
||||
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
|
||||
const primaryChildFragment = workInProgress.child;
|
||||
if (primaryChildFragment !== null) {
|
||||
// $FlowFixMe[unsafe-arithmetic] Flow doesn't support type casting in combination with the -= operator
|
||||
workInProgress.treeBaseDuration -=
|
||||
((primaryChildFragment.treeBaseDuration: any): number);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
emitPendingHydrationWarnings();
|
||||
// We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
|
||||
// state since we're now exiting out of it. popHydrationState doesn't do that for us.
|
||||
resetHydrationState();
|
||||
if ((workInProgress.flags & DidCapture) === NoFlags) {
|
||||
// This boundary did not suspend so it's now hydrated and unsuspended.
|
||||
nextState = workInProgress.memoizedState = null;
|
||||
}
|
||||
// If nothing suspended, we need to schedule an effect to mark this boundary
|
||||
// as having hydrated so events know that they're free to be invoked.
|
||||
// It's also a signal to replay events and the suspense callback.
|
||||
// If something suspended, schedule an effect to attach retry listeners.
|
||||
// So we might as well always mark this.
|
||||
workInProgress.flags |= Update;
|
||||
bubbleProperties(workInProgress);
|
||||
if (enableProfilerTimer) {
|
||||
if ((workInProgress.mode & ProfileMode) !== NoMode) {
|
||||
const isTimedOutSuspense = nextState !== null;
|
||||
if (isTimedOutSuspense) {
|
||||
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
|
||||
const primaryChildFragment = workInProgress.child;
|
||||
if (primaryChildFragment !== null) {
|
||||
// $FlowFixMe[unsafe-arithmetic] Flow doesn't support type casting in combination with the -= operator
|
||||
workInProgress.treeBaseDuration -=
|
||||
((primaryChildFragment.treeBaseDuration: any): number);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Successfully completed this tree. If this was a forced client render,
|
||||
// there may have been recoverable errors during first hydration
|
||||
// attempt. If so, add them to a queue so we can log them in the
|
||||
// commit phase. We also add them to prev state so we can get to them
|
||||
// from the Suspense Boundary.
|
||||
const hydrationErrors = upgradeHydrationErrorsToRecoverable();
|
||||
if (current !== null && current.memoizedState !== null) {
|
||||
const prevState: ActivityState = current.memoizedState;
|
||||
prevState.hydrationErrors = hydrationErrors;
|
||||
}
|
||||
// Fall through to normal Offscreen path
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function completeDehydratedSuspenseBoundary(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
@@ -938,7 +1022,7 @@ function completeDehydratedSuspenseBoundary(
|
||||
resetHydrationState();
|
||||
if ((workInProgress.flags & DidCapture) === NoFlags) {
|
||||
// This boundary did not suspend so it's now hydrated and unsuspended.
|
||||
workInProgress.memoizedState = null;
|
||||
nextState = workInProgress.memoizedState = null;
|
||||
}
|
||||
// If nothing suspended, we need to schedule an effect to mark this boundary
|
||||
// as having hydrated so events know that they're free to be invoked.
|
||||
@@ -1393,12 +1477,42 @@ function completeWork(
|
||||
return null;
|
||||
}
|
||||
case ActivityComponent: {
|
||||
if (current === null) {
|
||||
const wasHydrated = popHydrationState(workInProgress);
|
||||
if (wasHydrated) {
|
||||
// TODO: Implement prepareToHydrateActivityInstance
|
||||
const nextState: null | ActivityState = workInProgress.memoizedState;
|
||||
|
||||
if (current === null || current.memoizedState !== null) {
|
||||
const fallthroughToNormalOffscreenPath =
|
||||
completeDehydratedActivityBoundary(
|
||||
current,
|
||||
workInProgress,
|
||||
nextState,
|
||||
);
|
||||
if (!fallthroughToNormalOffscreenPath) {
|
||||
if (workInProgress.flags & ForceClientRender) {
|
||||
popSuspenseHandler(workInProgress);
|
||||
// Special case. There were remaining unhydrated nodes. We treat
|
||||
// this as a mismatch. Revert to client rendering.
|
||||
return workInProgress;
|
||||
} else {
|
||||
popSuspenseHandler(workInProgress);
|
||||
// Did not finish hydrating, either because this is the initial
|
||||
// render or because something suspended.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ((workInProgress.flags & DidCapture) !== NoFlags) {
|
||||
// We called retryActivityComponentWithoutHydrating and tried client rendering
|
||||
// but now we suspended again. We should never arrive here because we should
|
||||
// not have pushed a suspense handler during that second pass and it should
|
||||
// instead have suspended above.
|
||||
throw new Error(
|
||||
'Client rendering an Activity suspended it again. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
|
||||
// Continue with the normal Activity path.
|
||||
}
|
||||
|
||||
bubbleProperties(workInProgress);
|
||||
return null;
|
||||
}
|
||||
@@ -1443,7 +1557,6 @@ function completeWork(
|
||||
if ((workInProgress.flags & DidCapture) !== NoFlags) {
|
||||
// Something suspended. Re-render with the fallback children.
|
||||
workInProgress.lanes = renderLanes;
|
||||
// Do not reset the effect list.
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(workInProgress.mode & ProfileMode) !== NoMode
|
||||
|
||||
+80
-9
@@ -17,6 +17,7 @@ import type {
|
||||
Container,
|
||||
HostContext,
|
||||
} from './ReactFiberConfig';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
||||
import type {TreeContext} from './ReactFiberTreeContext';
|
||||
import type {CapturedValue} from './ReactCapturedValue';
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
describeHydratableInstanceForDevWarnings,
|
||||
hydrateTextInstance,
|
||||
diffHydratedTextForDevWarnings,
|
||||
hydrateActivityInstance,
|
||||
hydrateSuspenseInstance,
|
||||
getNextHydratableInstanceAfterActivityInstance,
|
||||
getNextHydratableInstanceAfterSuspenseInstance,
|
||||
@@ -175,6 +177,28 @@ function enterHydrationState(fiber: Fiber): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function reenterHydrationStateFromDehydratedActivityInstance(
|
||||
fiber: Fiber,
|
||||
activityInstance: ActivityInstance,
|
||||
treeContext: TreeContext | null,
|
||||
): boolean {
|
||||
if (!supportsHydration) {
|
||||
return false;
|
||||
}
|
||||
nextHydratableInstance =
|
||||
getFirstHydratableChildWithinActivityInstance(activityInstance);
|
||||
hydrationParentFiber = fiber;
|
||||
isHydrating = true;
|
||||
hydrationErrors = null;
|
||||
didSuspendOrErrorDEV = false;
|
||||
hydrationDiffRootDEV = null;
|
||||
rootOrSingletonContext = false;
|
||||
if (treeContext !== null) {
|
||||
restoreSuspendedTreeContext(fiber, treeContext);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function reenterHydrationStateFromDehydratedSuspenseInstance(
|
||||
fiber: Fiber,
|
||||
suspenseInstance: SuspenseInstance,
|
||||
@@ -281,18 +305,31 @@ function tryHydrateActivity(
|
||||
fiber: Fiber,
|
||||
nextInstance: any,
|
||||
): null | ActivityInstance {
|
||||
// fiber is a SuspenseComponent Fiber
|
||||
// fiber is a ActivityComponent Fiber
|
||||
const activityInstance = canHydrateActivityInstance(
|
||||
nextInstance,
|
||||
rootOrSingletonContext,
|
||||
);
|
||||
if (activityInstance !== null) {
|
||||
// TODO: Implement dehydrated Activity state.
|
||||
// TODO: Delete this from stateNode. It's only used to skip past it.
|
||||
fiber.stateNode = activityInstance;
|
||||
const activityState: ActivityState = {
|
||||
dehydrated: activityInstance,
|
||||
treeContext: getSuspendedTreeContext(),
|
||||
retryLane: OffscreenLane,
|
||||
hydrationErrors: null,
|
||||
};
|
||||
fiber.memoizedState = activityState;
|
||||
// Store the dehydrated fragment as a child fiber.
|
||||
// This simplifies the code for getHostSibling and deleting nodes,
|
||||
// since it doesn't have to consider all Suspense boundaries and
|
||||
// check if they're dehydrated ones or not.
|
||||
const dehydratedFragment =
|
||||
createFiberFromDehydratedFragment(activityInstance);
|
||||
dehydratedFragment.return = fiber;
|
||||
fiber.child = dehydratedFragment;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance =
|
||||
getFirstHydratableChildWithinActivityInstance(activityInstance);
|
||||
// While an Activity Instance does have children, we won't step into
|
||||
// it during the first pass. Instead, we'll reenter it later.
|
||||
nextHydratableInstance = null;
|
||||
}
|
||||
return activityInstance;
|
||||
}
|
||||
@@ -592,6 +629,27 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
|
||||
}
|
||||
}
|
||||
|
||||
function prepareToHydrateHostActivityInstance(fiber: Fiber): void {
|
||||
if (!supportsHydration) {
|
||||
throw new Error(
|
||||
'Expected prepareToHydrateHostActivityInstance() to never be called. ' +
|
||||
'This error is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
const activityState: null | ActivityState = fiber.memoizedState;
|
||||
const activityInstance: null | ActivityInstance =
|
||||
activityState !== null ? activityState.dehydrated : null;
|
||||
|
||||
if (!activityInstance) {
|
||||
throw new Error(
|
||||
'Expected to have a hydrated activity instance. ' +
|
||||
'This error is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
hydrateActivityInstance(activityInstance, fiber);
|
||||
}
|
||||
|
||||
function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void {
|
||||
if (!supportsHydration) {
|
||||
throw new Error(
|
||||
@@ -613,10 +671,22 @@ function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void {
|
||||
|
||||
hydrateSuspenseInstance(suspenseInstance, fiber);
|
||||
}
|
||||
|
||||
function skipPastDehydratedActivityInstance(
|
||||
fiber: Fiber,
|
||||
): null | HydratableInstance {
|
||||
return getNextHydratableInstanceAfterActivityInstance(fiber.stateNode);
|
||||
const activityState: null | ActivityState = fiber.memoizedState;
|
||||
const activityInstance: null | ActivityInstance =
|
||||
activityState !== null ? activityState.dehydrated : null;
|
||||
|
||||
if (!activityInstance) {
|
||||
throw new Error(
|
||||
'Expected to have a hydrated suspense instance. ' +
|
||||
'This error is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
return getNextHydratableInstanceAfterActivityInstance(activityInstance);
|
||||
}
|
||||
|
||||
function skipPastDehydratedSuspenseInstance(
|
||||
@@ -647,6 +717,7 @@ function popToNextHostParent(fiber: Fiber): void {
|
||||
while (hydrationParentFiber) {
|
||||
switch (hydrationParentFiber.tag) {
|
||||
case HostComponent:
|
||||
case ActivityComponent:
|
||||
case SuspenseComponent:
|
||||
rootOrSingletonContext = false;
|
||||
return;
|
||||
@@ -654,8 +725,6 @@ function popToNextHostParent(fiber: Fiber): void {
|
||||
case HostRoot:
|
||||
rootOrSingletonContext = true;
|
||||
return;
|
||||
case ActivityComponent:
|
||||
return;
|
||||
default:
|
||||
hydrationParentFiber = hydrationParentFiber.return;
|
||||
}
|
||||
@@ -834,6 +903,7 @@ export {
|
||||
warnIfHydrating,
|
||||
enterHydrationState,
|
||||
getIsHydrating,
|
||||
reenterHydrationStateFromDehydratedActivityInstance,
|
||||
reenterHydrationStateFromDehydratedSuspenseInstance,
|
||||
resetHydrationState,
|
||||
claimHydratableSingleton,
|
||||
@@ -843,6 +913,7 @@ export {
|
||||
claimNextHydratableSuspenseInstance,
|
||||
prepareToHydrateHostInstance,
|
||||
prepareToHydrateHostTextInstance,
|
||||
prepareToHydrateHostActivityInstance,
|
||||
prepareToHydrateHostSuspenseInstance,
|
||||
popHydrationState,
|
||||
};
|
||||
|
||||
+7
-3
@@ -21,6 +21,7 @@ import type {
|
||||
} from './ReactFiberConfig';
|
||||
import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
|
||||
import type {Lane} from './ReactFiberLane';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
||||
|
||||
import {LegacyRoot} from './ReactRootTags';
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
ClassComponent,
|
||||
HostRoot,
|
||||
SuspenseComponent,
|
||||
ActivityComponent,
|
||||
} from './ReactWorkTags';
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
import isArray from 'shared/isArray';
|
||||
@@ -484,6 +486,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActivityComponent:
|
||||
case SuspenseComponent: {
|
||||
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
|
||||
if (root !== null) {
|
||||
@@ -501,7 +504,8 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
|
||||
}
|
||||
|
||||
function markRetryLaneImpl(fiber: Fiber, retryLane: Lane) {
|
||||
const suspenseState: null | SuspenseState = fiber.memoizedState;
|
||||
const suspenseState: null | SuspenseState | ActivityState =
|
||||
fiber.memoizedState;
|
||||
if (suspenseState !== null && suspenseState.dehydrated !== null) {
|
||||
suspenseState.retryLane = higherPriorityLane(
|
||||
suspenseState.retryLane,
|
||||
@@ -520,7 +524,7 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) {
|
||||
}
|
||||
|
||||
export function attemptContinuousHydration(fiber: Fiber): void {
|
||||
if (fiber.tag !== SuspenseComponent) {
|
||||
if (fiber.tag !== SuspenseComponent && fiber.tag !== ActivityComponent) {
|
||||
// We ignore HostRoots here because we can't increase
|
||||
// their priority and they should not suspend on I/O,
|
||||
// since you have to wrap anything that might suspend in
|
||||
@@ -536,7 +540,7 @@ export function attemptContinuousHydration(fiber: Fiber): void {
|
||||
}
|
||||
|
||||
export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
|
||||
if (fiber.tag !== SuspenseComponent) {
|
||||
if (fiber.tag !== SuspenseComponent && fiber.tag !== ActivityComponent) {
|
||||
// We ignore HostRoots here because we can't increase
|
||||
// their priority other than synchronously flush it.
|
||||
return;
|
||||
|
||||
+16
-1
@@ -106,12 +106,27 @@ export function pushFallbackTreeSuspenseHandler(fiber: Fiber): void {
|
||||
reuseSuspenseHandlerOnStack(fiber);
|
||||
}
|
||||
|
||||
export function pushDehydratedActivitySuspenseHandler(fiber: Fiber): void {
|
||||
// This is called when hydrating an Activity boundary. We can just leave it
|
||||
// dehydrated if it suspends.
|
||||
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
|
||||
// Reuse the current value on the stack.
|
||||
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
|
||||
// into separate functions for Activity, Suspense and Offscreen.
|
||||
pushSuspenseListContext(fiber, suspenseStackCursor.current);
|
||||
push(suspenseHandlerStackCursor, fiber, fiber);
|
||||
if (shellBoundary === null) {
|
||||
// We can contain any suspense inside the Activity boundary.
|
||||
shellBoundary = fiber;
|
||||
}
|
||||
}
|
||||
|
||||
export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
|
||||
if (fiber.tag === OffscreenComponent) {
|
||||
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
|
||||
// Reuse the current value on the stack.
|
||||
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
|
||||
// into separate functions for Suspense and Offscreen.
|
||||
// into separate functions for Activity, Suspense and Offscreen.
|
||||
pushSuspenseListContext(fiber, suspenseStackCursor.current);
|
||||
push(suspenseHandlerStackCursor, fiber, fiber);
|
||||
if (shellBoundary === null) {
|
||||
|
||||
+8
-6
@@ -24,6 +24,7 @@ import {
|
||||
FunctionComponent,
|
||||
ForwardRef,
|
||||
SimpleMemoComponent,
|
||||
ActivityComponent,
|
||||
SuspenseComponent,
|
||||
OffscreenComponent,
|
||||
} from './ReactWorkTags';
|
||||
@@ -398,8 +399,9 @@ function throwException(
|
||||
const suspenseBoundary = getSuspenseHandler();
|
||||
if (suspenseBoundary !== null) {
|
||||
switch (suspenseBoundary.tag) {
|
||||
case ActivityComponent:
|
||||
case SuspenseComponent: {
|
||||
// If this suspense boundary is not already showing a fallback, mark
|
||||
// If this suspense/activity boundary is not already showing a fallback, mark
|
||||
// the in-progress render as suspended. We try to perform this logic
|
||||
// as soon as soon as possible during the render phase, so the work
|
||||
// loop can know things like whether it's OK to switch to other tasks,
|
||||
@@ -553,19 +555,19 @@ function throwException(
|
||||
(disableLegacyMode || sourceFiber.mode & ConcurrentMode)
|
||||
) {
|
||||
markDidThrowWhileHydratingDEV();
|
||||
const suspenseBoundary = getSuspenseHandler();
|
||||
const hydrationBoundary = getSuspenseHandler();
|
||||
// If the error was thrown during hydration, we may be able to recover by
|
||||
// discarding the dehydrated content and switching to a client render.
|
||||
// Instead of surfacing the error, find the nearest Suspense boundary
|
||||
// and render it again without hydration.
|
||||
if (suspenseBoundary !== null) {
|
||||
if ((suspenseBoundary.flags & ShouldCapture) === NoFlags) {
|
||||
if (hydrationBoundary !== null) {
|
||||
if ((hydrationBoundary.flags & ShouldCapture) === NoFlags) {
|
||||
// Set a flag to indicate that we should try rendering the normal
|
||||
// children again, not the fallback.
|
||||
suspenseBoundary.flags |= ForceClientRender;
|
||||
hydrationBoundary.flags |= ForceClientRender;
|
||||
}
|
||||
markSuspenseBoundaryShouldCapture(
|
||||
suspenseBoundary,
|
||||
hydrationBoundary,
|
||||
returnFiber,
|
||||
sourceFiber,
|
||||
root,
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
SuspenseInstance,
|
||||
Instance,
|
||||
} from './ReactFiberConfig';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
||||
|
||||
import {
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
HostRoot,
|
||||
HostPortal,
|
||||
HostText,
|
||||
ActivityComponent,
|
||||
SuspenseComponent,
|
||||
OffscreenComponent,
|
||||
} from './ReactWorkTags';
|
||||
@@ -82,6 +84,18 @@ export function getSuspenseInstanceFromFiber(
|
||||
export function getActivityInstanceFromFiber(
|
||||
fiber: Fiber,
|
||||
): null | ActivityInstance {
|
||||
if (fiber.tag === ActivityComponent) {
|
||||
let activityState: ActivityState | null = fiber.memoizedState;
|
||||
if (activityState === null) {
|
||||
const current = fiber.alternate;
|
||||
if (current !== null) {
|
||||
activityState = current.memoizedState;
|
||||
}
|
||||
}
|
||||
if (activityState !== null) {
|
||||
return activityState.dehydrated;
|
||||
}
|
||||
}
|
||||
// TODO: Implement this on ActivityComponent.
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
import type {Lanes} from './ReactFiberLane';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
||||
import type {Cache} from './ReactFiberCacheComponent';
|
||||
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
HostSingleton,
|
||||
HostPortal,
|
||||
ContextProvider,
|
||||
ActivityComponent,
|
||||
SuspenseComponent,
|
||||
SuspenseListComponent,
|
||||
OffscreenComponent,
|
||||
@@ -120,6 +122,35 @@ function unwindWork(
|
||||
popHostContext(workInProgress);
|
||||
return null;
|
||||
}
|
||||
case ActivityComponent: {
|
||||
const activityState: null | ActivityState = workInProgress.memoizedState;
|
||||
if (activityState !== null) {
|
||||
popSuspenseHandler(workInProgress);
|
||||
|
||||
if (workInProgress.alternate === null) {
|
||||
throw new Error(
|
||||
'Threw in newly mounted dehydrated component. This is likely a bug in ' +
|
||||
'React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
resetHydrationState();
|
||||
}
|
||||
|
||||
const flags = workInProgress.flags;
|
||||
if (flags & ShouldCapture) {
|
||||
workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
|
||||
// Captured a suspense effect. Re-render the boundary.
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(workInProgress.mode & ProfileMode) !== NoMode
|
||||
) {
|
||||
transferActualDuration(workInProgress);
|
||||
}
|
||||
return workInProgress;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
popSuspenseHandler(workInProgress);
|
||||
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
|
||||
@@ -242,6 +273,12 @@ function unwindInterruptedWork(
|
||||
case HostPortal:
|
||||
popHostContainer(interruptedWork);
|
||||
break;
|
||||
case ActivityComponent: {
|
||||
if (interruptedWork.memoizedState !== null) {
|
||||
popSuspenseHandler(interruptedWork);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SuspenseComponent:
|
||||
popSuspenseHandler(interruptedWork);
|
||||
break;
|
||||
|
||||
+5
-1
@@ -12,6 +12,7 @@ import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
|
||||
import type {Wakeable, Thenable} from 'shared/ReactTypes';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
import type {Lanes, Lane} from './ReactFiberLane';
|
||||
import type {ActivityState} from './ReactFiberActivityComponent';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
||||
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
|
||||
import type {Transition} from 'react/src/ReactStartTransition';
|
||||
@@ -124,6 +125,7 @@ import {
|
||||
import {
|
||||
HostRoot,
|
||||
ClassComponent,
|
||||
ActivityComponent,
|
||||
SuspenseComponent,
|
||||
SuspenseListComponent,
|
||||
OffscreenComponent,
|
||||
@@ -4489,9 +4491,11 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
|
||||
let retryLane: Lane = NoLane; // Default
|
||||
let retryCache: WeakSet<Wakeable> | Set<Wakeable> | null;
|
||||
switch (boundaryFiber.tag) {
|
||||
case ActivityComponent:
|
||||
case SuspenseComponent:
|
||||
retryCache = boundaryFiber.stateNode;
|
||||
const suspenseState: null | SuspenseState = boundaryFiber.memoizedState;
|
||||
const suspenseState: null | SuspenseState | ActivityState =
|
||||
boundaryFiber.memoizedState;
|
||||
if (suspenseState !== null) {
|
||||
retryLane = suspenseState.retryLane;
|
||||
}
|
||||
|
||||
+5
-5
@@ -2227,14 +2227,14 @@ function renderActivity(
|
||||
}
|
||||
} else {
|
||||
// Render
|
||||
// An Activity boundary is delimited so that we can hydrate it separately.
|
||||
pushStartActivityBoundary(segment.chunks, request.renderState);
|
||||
segment.lastPushedText = false;
|
||||
const mode = props.mode;
|
||||
if (mode === 'hidden') {
|
||||
// A hidden Activity boundary is not server rendered. Prerendering happens
|
||||
// on the client.
|
||||
} else {
|
||||
// An Activity boundary is delimited so that we can hydrate it separately.
|
||||
pushStartActivityBoundary(segment.chunks, request.renderState);
|
||||
segment.lastPushedText = false;
|
||||
// A visible Activity boundary has its children rendered inside the boundary.
|
||||
const prevKeyPath = task.keyPath;
|
||||
task.keyPath = keyPath;
|
||||
@@ -2242,9 +2242,9 @@ function renderActivity(
|
||||
// need to pop back up and finish the end comment.
|
||||
renderNode(request, task, props.children, -1);
|
||||
task.keyPath = prevKeyPath;
|
||||
pushEndActivityBoundary(segment.chunks, request.renderState);
|
||||
segment.lastPushedText = false;
|
||||
}
|
||||
pushEndActivityBoundary(segment.chunks, request.renderState);
|
||||
segment.lastPushedText = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -540,5 +540,8 @@
|
||||
"552": "Cannot use a startGestureTransition() on a detached root.",
|
||||
"553": "A Timeline is required as the first argument to startGestureTransition.",
|
||||
"554": "Cannot setState on regular state inside a startGestureTransition. Gestures can only update the useOptimistic() hook. There should be no side-effects associated with starting a Gesture until its Action is invoked. Move side-effects to the Action instead.",
|
||||
"555": "Cannot requestFormReset() inside a startGestureTransition. There should be no side-effects associated with starting a Gesture until its Action is invoked. Move side-effects to the Action instead."
|
||||
"555": "Cannot requestFormReset() inside a startGestureTransition. There should be no side-effects associated with starting a Gesture until its Action is invoked. Move side-effects to the Action instead.",
|
||||
"556": "Expected prepareToHydrateHostActivityInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.",
|
||||
"557": "Expected to have a hydrated activity instance. This error is likely caused by a bug in React. Please file an issue.",
|
||||
"558": "Client rendering an Activity suspended it again. This is a bug in React."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user