Implement ActivityInstance in FiberConfigDOM (#32842)

Stacked on #32851 and #32900.

This implements the equivalent Configs for ActivityInstance as we have
for SuspenseInstance. These can be implemented as comments but they
don't have to be and can be implemented differently in the renderer.

This seems like a lot duplication but it's actually ends mostly just
calling the same methods underneath and the wrappers compiles out.

This doesn't leave the Activity dehydrated yet. It just hydrates into it
immediately.
This commit is contained in:
Sebastian Markbåge 2025-04-22 19:44:14 -04:00 committed by GitHub
parent 3fbd6b7b50
commit 17f88c80ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 362 additions and 91 deletions

View File

@ -17,6 +17,7 @@ import type {
Container,
TextInstance,
Instance,
ActivityInstance,
SuspenseInstance,
Props,
HoistableRoot,
@ -30,9 +31,10 @@ import {
HostText,
HostRoot,
SuspenseComponent,
ActivityComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {getParentSuspenseInstance} from './ReactFiberConfigDOM';
import {getParentHydrationBoundary} from './ReactFiberConfigDOM';
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
@ -59,7 +61,12 @@ export function detachDeletedInstance(node: Instance): void {
export function precacheFiberNode(
hostInst: Fiber,
node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
node:
| Instance
| TextInstance
| SuspenseInstance
| ActivityInstance
| ReactScopeInstance,
): void {
(node: any)[internalInstanceKey] = hostInst;
}
@ -81,15 +88,16 @@ export function isContainerMarkedAsRoot(node: Container): boolean {
// Given a DOM node, return the closest HostComponent or HostText fiber ancestor.
// If the target node is part of a hydrated or not yet rendered subtree, then
// this may also return a SuspenseComponent or HostRoot to indicate that.
// this may also return a SuspenseComponent, ActivityComponent or HostRoot to
// indicate that.
// Conceptually the HostRoot fiber is a child of the Container node. So if you
// pass the Container node as the targetNode, you will not actually get the
// HostRoot back. To get to the HostRoot, you need to pass a child of it.
// The same thing applies to Suspense boundaries.
// The same thing applies to Suspense and Activity boundaries.
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
let targetInst = (targetNode: any)[internalInstanceKey];
if (targetInst) {
// Don't return HostRoot or SuspenseComponent here.
// Don't return HostRoot, SuspenseComponent or ActivityComponent here.
return targetInst;
}
// If the direct event target isn't a React owned DOM node, we need to look
@ -129,8 +137,8 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
) {
// Next we need to figure out if the node that skipped past is
// nested within a dehydrated boundary and if so, which one.
let suspenseInstance = getParentSuspenseInstance(targetNode);
while (suspenseInstance !== null) {
let hydrationInstance = getParentHydrationBoundary(targetNode);
while (hydrationInstance !== null) {
// We found a suspense instance. That means that we haven't
// hydrated it yet. Even though we leave the comments in the
// DOM after hydrating, and there are boundaries in the DOM
@ -140,15 +148,15 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
// Let's get the fiber associated with the SuspenseComponent
// as the deepest instance.
// $FlowFixMe[prop-missing]
const targetSuspenseInst = suspenseInstance[internalInstanceKey];
if (targetSuspenseInst) {
return targetSuspenseInst;
const targetFiber = hydrationInstance[internalInstanceKey];
if (targetFiber) {
return targetFiber;
}
// If we don't find a Fiber on the comment, it might be because
// we haven't gotten to hydrate it yet. There might still be a
// parent boundary that hasn't above this one so we need to find
// the outer most that is known.
suspenseInstance = getParentSuspenseInstance(suspenseInstance);
hydrationInstance = getParentHydrationBoundary(hydrationInstance);
// If we don't find one, then that should mean that the parent
// host component also hasn't hydrated yet. We can return it
// below since it will bail out on the isMounted check later.
@ -176,6 +184,7 @@ export function getInstanceFromNode(node: Node): Fiber | null {
tag === HostComponent ||
tag === HostText ||
tag === SuspenseComponent ||
tag === ActivityComponent ||
tag === HostHoistable ||
tag === HostSingleton ||
tag === HostRoot
@ -211,15 +220,17 @@ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
}
export function getFiberCurrentPropsFromNode(
node: Container | Instance | TextInstance | SuspenseInstance,
node:
| Container
| Instance
| TextInstance
| SuspenseInstance
| ActivityInstance,
): Props {
return (node: any)[internalPropsKey] || null;
}
export function updateFiberProps(
node: Instance | TextInstance | SuspenseInstance,
props: Props,
): void {
export function updateFiberProps(node: Instance, props: Props): void {
(node: any)[internalPropsKey] = props;
}

View File

@ -187,13 +187,20 @@ export type Container =
| interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
export type Instance = Element;
export type TextInstance = Text;
export interface SuspenseInstance extends Comment {
_reactRetry?: () => void;
declare class ActivityInterface extends Comment {}
declare class SuspenseInterface extends Comment {
_reactRetry: void | (() => void);
}
export type ActivityInstance = ActivityInterface;
export type SuspenseInstance = SuspenseInterface;
type FormStateMarkerInstance = Comment;
export type HydratableInstance =
| Instance
| TextInstance
| ActivityInstance
| SuspenseInstance
| FormStateMarkerInstance;
export type PublicInstance = Element | Text;
@ -226,6 +233,8 @@ type SelectionInformation = {
const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
const ACTIVITY_START_DATA = '&';
const ACTIVITY_END_DATA = '/&';
const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
@ -947,7 +956,7 @@ export function appendChildToContainer(
export function insertBefore(
parentInstance: Instance,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
if (supportsMoveBefore && child.parentNode !== null) {
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
@ -960,7 +969,7 @@ export function insertBefore(
export function insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
if (__DEV__) {
warnForReactChildrenConflict(container);
@ -1024,14 +1033,14 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
export function removeChild(
parentInstance: Instance,
child: Instance | TextInstance | SuspenseInstance,
child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
parentInstance.removeChild(child);
}
export function removeChildFromContainer(
container: Container,
child: Instance | TextInstance | SuspenseInstance,
child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
): void {
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
@ -1049,11 +1058,11 @@ export function removeChildFromContainer(
parentNode.removeChild(child);
}
export function clearSuspenseBoundary(
function clearHydrationBoundary(
parentInstance: Instance,
suspenseInstance: SuspenseInstance,
hydrationInstance: SuspenseInstance | ActivityInstance,
): void {
let node: Node = suspenseInstance;
let node: Node = hydrationInstance;
// Delete all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
// deep we are and only break out when we're back on top.
@ -1063,11 +1072,11 @@ export function clearSuspenseBoundary(
parentInstance.removeChild(node);
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
const data = ((nextNode: any).data: string);
if (data === SUSPENSE_END_DATA) {
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
parentInstance.removeChild(nextNode);
// Retry if any event replaying was blocked on this.
retryIfBlockedOn(suspenseInstance);
retryIfBlockedOn(hydrationInstance);
return;
} else {
depth--;
@ -1075,7 +1084,8 @@ export function clearSuspenseBoundary(
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
} else if (data === PREAMBLE_CONTRIBUTION_HTML) {
@ -1102,12 +1112,26 @@ export function clearSuspenseBoundary(
} while (node);
// TODO: Warn, we didn't find the end comment boundary.
// Retry if any event replaying was blocked on this.
retryIfBlockedOn(suspenseInstance);
retryIfBlockedOn(hydrationInstance);
}
export function clearSuspenseBoundaryFromContainer(
container: Container,
export function clearActivityBoundary(
parentInstance: Instance,
activityInstance: ActivityInstance,
): void {
clearHydrationBoundary(parentInstance, activityInstance);
}
export function clearSuspenseBoundary(
parentInstance: Instance,
suspenseInstance: SuspenseInstance,
): void {
clearHydrationBoundary(parentInstance, suspenseInstance);
}
function clearHydrationBoundaryFromContainer(
container: Container,
hydrationInstance: SuspenseInstance | ActivityInstance,
): void {
let parentNode: DocumentFragment | Element;
if (container.nodeType === DOCUMENT_NODE) {
@ -1122,13 +1146,27 @@ export function clearSuspenseBoundaryFromContainer(
} else {
parentNode = (container: any);
}
clearSuspenseBoundary(parentNode, suspenseInstance);
clearHydrationBoundary(parentNode, hydrationInstance);
// Retry if any event replaying was blocked on this.
retryIfBlockedOn(container);
}
function hideOrUnhideSuspenseBoundary(
export function clearActivityBoundaryFromContainer(
container: Container,
activityInstance: ActivityInstance,
): void {
clearHydrationBoundaryFromContainer(container, activityInstance);
}
export function clearSuspenseBoundaryFromContainer(
container: Container,
suspenseInstance: SuspenseInstance,
): void {
clearHydrationBoundaryFromContainer(container, suspenseInstance);
}
function hideOrUnhideDehydratedBoundary(
suspenseInstance: SuspenseInstance | ActivityInstance,
isHidden: boolean,
) {
let node: Node = suspenseInstance;
@ -1178,8 +1216,10 @@ function hideOrUnhideSuspenseBoundary(
} while (node);
}
export function hideSuspenseBoundary(suspenseInstance: SuspenseInstance): void {
hideOrUnhideSuspenseBoundary(suspenseInstance, true);
export function hideDehydratedBoundary(
suspenseInstance: SuspenseInstance,
): void {
hideOrUnhideDehydratedBoundary(suspenseInstance, true);
}
export function hideInstance(instance: Instance): void {
@ -1199,10 +1239,10 @@ export function hideTextInstance(textInstance: TextInstance): void {
textInstance.nodeValue = '';
}
export function unhideSuspenseBoundary(
suspenseInstance: SuspenseInstance,
export function unhideDehydratedBoundary(
dehydratedInstance: SuspenseInstance | ActivityInstance,
): void {
hideOrUnhideSuspenseBoundary(suspenseInstance, false);
hideOrUnhideDehydratedBoundary(dehydratedInstance, false);
}
export function unhideInstance(instance: Instance, props: Props): void {
@ -3047,10 +3087,10 @@ export function canHydrateTextInstance(
return ((instance: any): TextInstance);
}
export function canHydrateSuspenseInstance(
function canHydrateHydrationBoundary(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | SuspenseInstance {
): null | SuspenseInstance | ActivityInstance {
while (instance.nodeType !== COMMENT_NODE) {
if (!inRootOrSingleton) {
return null;
@ -3061,8 +3101,42 @@ export function canHydrateSuspenseInstance(
}
instance = nextInstance;
}
// This has now been refined to a suspense node.
return ((instance: any): SuspenseInstance);
// This has now been refined to a hydration boundary node.
return (instance: any);
}
export function canHydrateActivityInstance(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | ActivityInstance {
const hydratableInstance = canHydrateHydrationBoundary(
instance,
inRootOrSingleton,
);
if (
hydratableInstance !== null &&
hydratableInstance.data === ACTIVITY_START_DATA
) {
return (hydratableInstance: any);
}
return null;
}
export function canHydrateSuspenseInstance(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | SuspenseInstance {
const hydratableInstance = canHydrateHydrationBoundary(
instance,
inRootOrSingleton,
);
if (
hydratableInstance !== null &&
hydratableInstance.data !== ACTIVITY_START_DATA
) {
return (hydratableInstance: any);
}
return null;
}
export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {
@ -3186,12 +3260,13 @@ function getNextHydratable(node: ?Node) {
nodeData === SUSPENSE_START_DATA ||
nodeData === SUSPENSE_FALLBACK_START_DATA ||
nodeData === SUSPENSE_PENDING_START_DATA ||
nodeData === ACTIVITY_START_DATA ||
nodeData === FORM_STATE_IS_MATCHING ||
nodeData === FORM_STATE_IS_NOT_MATCHING
) {
break;
}
if (nodeData === SUSPENSE_END_DATA) {
if (nodeData === SUSPENSE_END_DATA || nodeData === ACTIVITY_END_DATA) {
return null;
}
}
@ -3230,6 +3305,12 @@ export function getFirstHydratableChildWithinContainer(
return getNextHydratable(parentElement.firstChild);
}
export function getFirstHydratableChildWithinActivityInstance(
parentInstance: ActivityInstance,
): null | HydratableInstance {
return getNextHydratable(parentInstance.nextSibling);
}
export function getFirstHydratableChildWithinSuspenseInstance(
parentInstance: SuspenseInstance,
): null | HydratableInstance {
@ -3281,6 +3362,12 @@ export function describeHydratableInstanceForDevWarnings(
props: getPropsFromElement((instance: any)),
};
} else if (instance.nodeType === COMMENT_NODE) {
if (instance.data === ACTIVITY_START_DATA) {
return {
type: 'Activity',
props: {},
};
}
return {
type: 'Suspense',
props: {},
@ -3372,6 +3459,13 @@ export function diffHydratedTextForDevWarnings(
return null;
}
export function hydrateActivityInstance(
activityInstance: ActivityInstance,
internalInstanceHandle: Object,
) {
precacheFiberNode(internalInstanceHandle, activityInstance);
}
export function hydrateSuspenseInstance(
suspenseInstance: SuspenseInstance,
internalInstanceHandle: Object,
@ -3379,10 +3473,10 @@ export function hydrateSuspenseInstance(
precacheFiberNode(internalInstanceHandle, suspenseInstance);
}
export function getNextHydratableInstanceAfterSuspenseInstance(
suspenseInstance: SuspenseInstance,
function getNextHydratableInstanceAfterHydrationBoundary(
hydrationInstance: SuspenseInstance | ActivityInstance,
): null | HydratableInstance {
let node = suspenseInstance.nextSibling;
let node = hydrationInstance.nextSibling;
// Skip past all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
// deep we are and only break out when we're back on top.
@ -3390,7 +3484,7 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
while (node) {
if (node.nodeType === COMMENT_NODE) {
const data = ((node: any).data: string);
if (data === SUSPENSE_END_DATA) {
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
return getNextHydratableSibling((node: any));
} else {
@ -3399,7 +3493,8 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA
data === SUSPENSE_PENDING_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
}
@ -3410,12 +3505,24 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
return null;
}
export function getNextHydratableInstanceAfterActivityInstance(
activityInstance: ActivityInstance,
): null | HydratableInstance {
return getNextHydratableInstanceAfterHydrationBoundary(activityInstance);
}
export function getNextHydratableInstanceAfterSuspenseInstance(
suspenseInstance: SuspenseInstance,
): null | HydratableInstance {
return getNextHydratableInstanceAfterHydrationBoundary(suspenseInstance);
}
// Returns the SuspenseInstance if this node is a direct child of a
// SuspenseInstance. I.e. if its previous sibling is a Comment with
// SUSPENSE_x_START_DATA. Otherwise, null.
export function getParentSuspenseInstance(
export function getParentHydrationBoundary(
targetInstance: Node,
): null | SuspenseInstance {
): null | SuspenseInstance | ActivityInstance {
let node = targetInstance.previousSibling;
// Skip past all nodes within this suspense boundary.
// There might be nested nodes so we need to keep track of how
@ -3427,14 +3534,15 @@ export function getParentSuspenseInstance(
if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA
data === SUSPENSE_PENDING_START_DATA ||
data === ACTIVITY_START_DATA
) {
if (depth === 0) {
return ((node: any): SuspenseInstance);
return ((node: any): SuspenseInstance | ActivityInstance);
} else {
depth--;
}
} else if (data === SUSPENSE_END_DATA) {
} else if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
depth++;
}
}
@ -3448,6 +3556,13 @@ export function commitHydratedContainer(container: Container): void {
retryIfBlockedOn(container);
}
export function commitHydratedActivityInstance(
activityInstance: ActivityInstance,
): void {
// Retry if any event replaying was blocked on this.
retryIfBlockedOn(activityInstance);
}
export function commitHydratedSuspenseInstance(
suspenseInstance: SuspenseInstance,
): void {

View File

@ -10,7 +10,11 @@
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
import type {AnyNativeEvent} from '../events/PluginModuleType';
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {Container, SuspenseInstance} from '../client/ReactFiberConfigDOM';
import type {
Container,
ActivityInstance,
SuspenseInstance,
} from '../client/ReactFiberConfigDOM';
import type {DOMEventName} from '../events/DOMEventNames';
import {
@ -22,9 +26,14 @@ import {attemptSynchronousHydration} from 'react-reconciler/src/ReactFiberReconc
import {
getNearestMountedFiber,
getContainerFromFiber,
getActivityInstanceFromFiber,
getSuspenseInstanceFromFiber,
} from 'react-reconciler/src/ReactFiberTreeReflection';
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {
HostRoot,
ActivityComponent,
SuspenseComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {type EventSystemFlags, IS_CAPTURE_PHASE} from './EventSystemFlags';
import getEventTarget from './getEventTarget';
@ -227,18 +236,18 @@ export function dispatchEvent(
export function findInstanceBlockingEvent(
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
): null | Container | SuspenseInstance | ActivityInstance {
const nativeEventTarget = getEventTarget(nativeEvent);
return findInstanceBlockingTarget(nativeEventTarget);
}
export let return_targetInst: null | Fiber = null;
// Returns a SuspenseInstance or Container if it's blocked.
// Returns a SuspenseInstance, ActivityInstance or Container if it's blocked.
// The return_targetInst field above is conceptually part of the return value.
export function findInstanceBlockingTarget(
targetNode: Node,
): null | Container | SuspenseInstance {
): null | Container | SuspenseInstance | ActivityInstance {
// TODO: Warn if _enabled is false.
return_targetInst = null;
@ -265,6 +274,19 @@ export function findInstanceBlockingTarget(
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === ActivityComponent) {
const instance = getActivityInstanceFromFiber(nearestMounted);
if (instance !== null) {
// Queue the event to be replayed later. Abort dispatching since we
// don't want this event dispatched twice through the event system.
// TODO: If this is the first discrete event in the queue. Schedule an increased
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (isRootDehydrated(root)) {

View File

@ -8,7 +8,11 @@
*/
import type {AnyNativeEvent} from '../events/PluginModuleType';
import type {Container, SuspenseInstance} from '../client/ReactFiberConfigDOM';
import type {
Container,
ActivityInstance,
SuspenseInstance,
} from '../client/ReactFiberConfigDOM';
import type {DOMEventName} from '../events/DOMEventNames';
import type {EventSystemFlags} from './EventSystemFlags';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
@ -21,6 +25,7 @@ import {
import {
getNearestMountedFiber,
getContainerFromFiber,
getActivityInstanceFromFiber,
getSuspenseInstanceFromFiber,
} from 'react-reconciler/src/ReactFiberTreeReflection';
import {
@ -33,7 +38,11 @@ import {
getClosestInstanceFromNode,
getFiberCurrentPropsFromNode,
} from '../client/ReactDOMComponentTree';
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {
HostRoot,
ActivityComponent,
SuspenseComponent,
} from 'react-reconciler/src/ReactWorkTags';
import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
import {dispatchReplayedFormAction} from './plugins/FormActionEventPlugin';
@ -56,7 +65,7 @@ type PointerEvent = Event & {
};
type QueuedReplayableEvent = {
blockedOn: null | Container | SuspenseInstance,
blockedOn: null | Container | ActivityInstance | SuspenseInstance,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
@ -76,7 +85,7 @@ const queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
// We could consider replaying selectionchange and touchmoves too.
type QueuedHydrationTarget = {
blockedOn: null | Container | SuspenseInstance,
blockedOn: null | Container | ActivityInstance | SuspenseInstance,
target: Node,
priority: EventPriority,
};
@ -120,7 +129,7 @@ export function isDiscreteEventThatRequiresHydration(
}
function createQueuedReplayableEvent(
blockedOn: null | Container | SuspenseInstance,
blockedOn: null | Container | ActivityInstance | SuspenseInstance,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
@ -170,7 +179,7 @@ export function clearIfContinuousEvent(
function accumulateOrCreateContinuousQueuedReplayableEvent(
existingQueuedEvent: null | QueuedReplayableEvent,
blockedOn: null | Container | SuspenseInstance,
blockedOn: null | Container | ActivityInstance | SuspenseInstance,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
@ -212,7 +221,7 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
}
export function queueIfContinuousEvent(
blockedOn: null | Container | SuspenseInstance,
blockedOn: null | Container | ActivityInstance | SuspenseInstance,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
@ -316,6 +325,18 @@ function attemptExplicitHydrationTarget(
attemptHydrationAtCurrentPriority(nearestMounted);
});
return;
}
} else if (tag === ActivityComponent) {
const instance = getActivityInstanceFromFiber(nearestMounted);
if (instance !== null) {
// We're blocked on hydrating this boundary.
// Increase its priority.
queuedTarget.blockedOn = instance;
attemptHydrationAtPriority(queuedTarget.priority, () => {
attemptHydrationAtCurrentPriority(nearestMounted);
});
return;
}
} else if (tag === HostRoot) {
@ -418,7 +439,7 @@ function replayUnblockedEvents() {
function scheduleCallbackIfUnblocked(
queuedEvent: QueuedReplayableEvent,
unblocked: Container | SuspenseInstance,
unblocked: Container | SuspenseInstance | ActivityInstance,
) {
if (queuedEvent.blockedOn === unblocked) {
queuedEvent.blockedOn = null;
@ -494,7 +515,7 @@ function scheduleReplayQueueIfNeeded(formReplayingQueue: FormReplayingQueue) {
}
export function retryIfBlockedOn(
unblocked: Container | SuspenseInstance,
unblocked: Container | SuspenseInstance | ActivityInstance,
): void {
if (queuedFocus !== null) {
scheduleCallbackIfUnblocked(queuedFocus, unblocked);

View File

@ -4,7 +4,7 @@
export const clientRenderBoundary =
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
export const completeBoundary =
'$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
'$RC=function(b,d,e){d=document.getElementById(d);d.parentNode.removeChild(d);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var c=a.data;if("/$"===c||"/&"===c)if(0===f)break;else f--;else"$"!==c&&"$?"!==c&&"$!"!==c&&"&"!==c||f++}c=a.nextSibling;e.removeChild(a);a=c}while(a);for(;d.firstChild;)e.insertBefore(d.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};';
export const completeBoundaryWithStyles =
'$RM=new Map;\n$RR=function(t,u,y){function v(n){this._p=null;n()}for(var w=$RC,p=$RM,q=new Map,r=document,g,b,h=r.querySelectorAll("link[data-precedence],style[data-precedence]"),x=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?x.push(b):("LINK"===b.tagName&&p.set(b.getAttribute("href"),b),q.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=y[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=p.get(d)){var f=a._p;c=!0}else{a=r.createElement("link");a.href=\nd;a.rel="stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,z){a.onload=v.bind(a,n);a.onerror=v.bind(a,z)});p.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=x[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=q.get(l)||g;c===g&&(g=a);q.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=r.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then(w.bind(null,\nt,u,""),w.bind(null,t,u,"Resource failed to load"))};';
export const completeSegment =

View File

@ -3,11 +3,13 @@
// Shared implementation and constants between the inline script and external
// runtime instruction sets.
export const COMMENT_NODE = 8;
export const SUSPENSE_START_DATA = '$';
export const SUSPENSE_END_DATA = '/$';
export const SUSPENSE_PENDING_START_DATA = '$?';
export const SUSPENSE_FALLBACK_START_DATA = '$!';
const COMMENT_NODE = 8;
const ACTIVITY_START_DATA = '&';
const ACTIVITY_END_DATA = '/&';
const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_FALLBACK_START_DATA = '$!';
// TODO: Symbols that are referenced outside this module use dynamic accessor
// notation instead of dot notation to prevent Closure's advanced compilation
@ -74,7 +76,7 @@ export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
do {
if (node && node.nodeType === COMMENT_NODE) {
const data = node.data;
if (data === SUSPENSE_END_DATA) {
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
break;
} else {
@ -83,7 +85,8 @@ export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
}

View File

@ -47,8 +47,8 @@ export type CreateRootOptions = {
export type HydrateRootOptions = {
// Hydration options
onHydrated?: (suspenseNode: Comment) => void,
onDeleted?: (suspenseNode: Comment) => void,
onHydrated?: (hydrationBoundary: Comment) => void,
onDeleted?: (hydrationBoundary: Comment) => void,
// Options for all roots
unstable_strictMode?: boolean,
unstable_transitionCallbacks?: TransitionTracingCallbacks,

View File

@ -244,6 +244,7 @@ import {
claimHydratableSingleton,
tryToClaimNextHydratableInstance,
tryToClaimNextHydratableTextInstance,
claimNextHydratableActivityInstance,
claimNextHydratableSuspenseInstance,
warnIfHydrating,
queueHydrationError,
@ -905,6 +906,10 @@ function updateActivityComponent(
};
if (current === null) {
if (getIsHydrating()) {
claimNextHydratableActivityInstance(workInProgress);
}
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
offscreenChildProps,
mode,

View File

@ -41,10 +41,10 @@ import {
insertBefore,
insertInContainerBefore,
replaceContainerChildren,
hideSuspenseBoundary,
hideDehydratedBoundary,
hideInstance,
hideTextInstance,
unhideSuspenseBoundary,
unhideDehydratedBoundary,
unhideInstance,
unhideTextInstance,
commitHydratedContainer,
@ -159,15 +159,15 @@ export function commitShowHideSuspenseBoundary(node: Fiber, isHidden: boolean) {
const instance = node.stateNode;
if (isHidden) {
if (__DEV__) {
runWithFiberInDEV(node, hideSuspenseBoundary, instance);
runWithFiberInDEV(node, hideDehydratedBoundary, instance);
} else {
hideSuspenseBoundary(instance);
hideDehydratedBoundary(instance);
}
} else {
if (__DEV__) {
runWithFiberInDEV(node, unhideSuspenseBoundary, node.stateNode);
runWithFiberInDEV(node, unhideDehydratedBoundary, node.stateNode);
} else {
unhideSuspenseBoundary(node.stateNode);
unhideDehydratedBoundary(node.stateNode);
}
}
} catch (error) {

View File

@ -997,7 +997,6 @@ function completeWork(
}
// Fallthrough
}
case ActivityComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
@ -1393,6 +1392,16 @@ function completeWork(
bubbleProperties(workInProgress);
return null;
}
case ActivityComponent: {
if (current === null) {
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Implement prepareToHydrateActivityInstance
}
}
bubbleProperties(workInProgress);
return null;
}
case SuspenseComponent: {
const nextState: null | SuspenseState = workInProgress.memoizedState;

View File

@ -19,6 +19,7 @@ function shim(...args: any): empty {
}
// Hydration (when unsupported)
export type ActivityInstance = mixed;
export type SuspenseInstance = mixed;
export const supportsHydration = false;
export const isSuspenseInstancePending = shim;
@ -31,21 +32,28 @@ export const getNextHydratableSibling = shim;
export const getNextHydratableSiblingAfterSingleton = shim;
export const getFirstHydratableChild = shim;
export const getFirstHydratableChildWithinContainer = shim;
export const getFirstHydratableChildWithinActivityInstance = shim;
export const getFirstHydratableChildWithinSuspenseInstance = shim;
export const getFirstHydratableChildWithinSingleton = shim;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
export const canHydrateActivityInstance = shim;
export const canHydrateSuspenseInstance = shim;
export const hydrateInstance = shim;
export const hydrateTextInstance = shim;
export const hydrateActivityInstance = shim;
export const hydrateSuspenseInstance = shim;
export const getNextHydratableInstanceAfterActivityInstance = shim;
export const getNextHydratableInstanceAfterSuspenseInstance = shim;
export const commitHydratedContainer = shim;
export const commitHydratedActivityInstance = shim;
export const commitHydratedSuspenseInstance = shim;
export const clearActivityBoundary = shim;
export const clearSuspenseBoundary = shim;
export const clearActivityBoundaryFromContainer = shim;
export const clearSuspenseBoundaryFromContainer = shim;
export const hideSuspenseBoundary = shim;
export const unhideSuspenseBoundary = shim;
export const hideDehydratedBoundary = shim;
export const unhideDehydratedBoundary = shim;
export const shouldDeleteUnhydratedTailInstances = shim;
export const diffHydratedPropsForDevWarnings = shim;
export const diffHydratedTextForDevWarnings = shim;

View File

@ -12,6 +12,7 @@ import type {
Instance,
TextInstance,
HydratableInstance,
ActivityInstance,
SuspenseInstance,
Container,
HostContext,
@ -26,6 +27,7 @@ import {
HostSingleton,
HostRoot,
SuspenseComponent,
ActivityComponent,
} from './ReactWorkTags';
import {favorSafetyOverHydrationPerf} from 'shared/ReactFeatureFlags';
@ -40,6 +42,7 @@ import {
getNextHydratableSiblingAfterSingleton,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinActivityInstance,
getFirstHydratableChildWithinSuspenseInstance,
getFirstHydratableChildWithinSingleton,
hydrateInstance,
@ -48,11 +51,13 @@ import {
hydrateTextInstance,
diffHydratedTextForDevWarnings,
hydrateSuspenseInstance,
getNextHydratableInstanceAfterActivityInstance,
getNextHydratableInstanceAfterSuspenseInstance,
shouldDeleteUnhydratedTailInstances,
resolveSingletonInstance,
canHydrateInstance,
canHydrateTextInstance,
canHydrateActivityInstance,
canHydrateSuspenseInstance,
canHydrateFormStateMarker,
isFormStateMarkerMatching,
@ -272,6 +277,26 @@ function tryHydrateText(fiber: Fiber, nextInstance: any) {
return false;
}
function tryHydrateActivity(
fiber: Fiber,
nextInstance: any,
): null | ActivityInstance {
// fiber is a SuspenseComponent 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;
hydrationParentFiber = fiber;
nextHydratableInstance =
getFirstHydratableChildWithinActivityInstance(activityInstance);
}
return activityInstance;
}
function tryHydrateSuspense(
fiber: Fiber,
nextInstance: any,
@ -425,6 +450,18 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
}
}
function claimNextHydratableActivityInstance(fiber: Fiber): ActivityInstance {
const nextInstance = nextHydratableInstance;
const activityInstance = nextInstance
? tryHydrateActivity(fiber, nextInstance)
: null;
if (activityInstance === null) {
warnNonHydratedInstance(fiber, nextInstance);
throw throwOnHydrationMismatch(fiber);
}
return activityInstance;
}
function claimNextHydratableSuspenseInstance(fiber: Fiber): SuspenseInstance {
const nextInstance = nextHydratableInstance;
const suspenseInstance = nextInstance
@ -576,6 +613,11 @@ function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void {
hydrateSuspenseInstance(suspenseInstance, fiber);
}
function skipPastDehydratedActivityInstance(
fiber: Fiber,
): null | HydratableInstance {
return getNextHydratableInstanceAfterActivityInstance(fiber.stateNode);
}
function skipPastDehydratedSuspenseInstance(
fiber: Fiber,
@ -612,6 +654,8 @@ function popToNextHostParent(fiber: Fiber): void {
case HostRoot:
rootOrSingletonContext = true;
return;
case ActivityComponent:
return;
default:
hydrationParentFiber = hydrationParentFiber.return;
}
@ -677,6 +721,8 @@ function popHydrationState(fiber: Fiber): boolean {
popToNextHostParent(fiber);
if (tag === SuspenseComponent) {
nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
} else if (tag === ActivityComponent) {
nextHydratableInstance = skipPastDehydratedActivityInstance(fiber);
} else if (supportsSingletons && tag === HostSingleton) {
nextHydratableInstance = getNextHydratableSiblingAfterSingleton(
fiber.type,
@ -793,6 +839,7 @@ export {
claimHydratableSingleton,
tryToClaimNextHydratableInstance,
tryToClaimNextHydratableTextInstance,
claimNextHydratableActivityInstance,
claimNextHydratableSuspenseInstance,
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,

View File

@ -14,6 +14,7 @@ import {
HostHoistable,
HostSingleton,
LazyComponent,
ActivityComponent,
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
@ -83,6 +84,8 @@ function describeFiberType(fiber: Fiber): null | string {
return fiber.type;
case LazyComponent:
return 'Lazy';
case ActivityComponent:
return 'Activity';
case SuspenseComponent:
return 'Suspense';
case SuspenseListComponent:

View File

@ -8,7 +8,12 @@
*/
import type {Fiber} from './ReactInternalTypes';
import type {Container, SuspenseInstance, Instance} from './ReactFiberConfig';
import type {
Container,
ActivityInstance,
SuspenseInstance,
Instance,
} from './ReactFiberConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {
@ -74,6 +79,13 @@ export function getSuspenseInstanceFromFiber(
return null;
}
export function getActivityInstanceFromFiber(
fiber: Fiber,
): null | ActivityInstance {
// TODO: Implement this on ActivityComponent.
return null;
}
export function getContainerFromFiber(fiber: Fiber): null | Container {
return fiber.tag === HostRoot
? (fiber.stateNode.containerInfo: Container)

View File

@ -29,6 +29,7 @@ import type {
Instance,
TimeoutHandle,
NoTimeout,
ActivityInstance,
SuspenseInstance,
TransitionStatus,
} from './ReactFiberConfig';
@ -297,8 +298,10 @@ type UpdaterTrackingOnlyFiberRootProperties = {
};
export type SuspenseHydrationCallbacks = {
onHydrated?: (suspenseInstance: SuspenseInstance) => void,
onDeleted?: (suspenseInstance: SuspenseInstance) => void,
+onHydrated?: (
hydrationBoundary: SuspenseInstance | ActivityInstance,
) => void,
+onDeleted?: (hydrationBoundary: SuspenseInstance | ActivityInstance) => void,
...
};

View File

@ -29,6 +29,7 @@ export opaque type Props = mixed;
export opaque type Container = mixed;
export opaque type Instance = mixed;
export opaque type TextInstance = mixed;
export opaque type ActivityInstance = mixed;
export opaque type SuspenseInstance = mixed;
export opaque type HydratableInstance = mixed;
export opaque type PublicInstance = mixed;
@ -202,26 +203,37 @@ export const getNextHydratableSiblingAfterSingleton =
export const getFirstHydratableChild = $$$config.getFirstHydratableChild;
export const getFirstHydratableChildWithinContainer =
$$$config.getFirstHydratableChildWithinContainer;
export const getFirstHydratableChildWithinActivityInstance =
$$$config.getFirstHydratableChildWithinActivityInstance;
export const getFirstHydratableChildWithinSuspenseInstance =
$$$config.getFirstHydratableChildWithinSuspenseInstance;
export const getFirstHydratableChildWithinSingleton =
$$$config.getFirstHydratableChildWithinSingleton;
export const canHydrateInstance = $$$config.canHydrateInstance;
export const canHydrateTextInstance = $$$config.canHydrateTextInstance;
export const canHydrateActivityInstance = $$$config.canHydrateActivityInstance;
export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance;
export const hydrateInstance = $$$config.hydrateInstance;
export const hydrateTextInstance = $$$config.hydrateTextInstance;
export const hydrateActivityInstance = $$$config.hydrateActivityInstance;
export const hydrateSuspenseInstance = $$$config.hydrateSuspenseInstance;
export const getNextHydratableInstanceAfterActivityInstance =
$$$config.getNextHydratableInstanceAfterActivityInstance;
export const getNextHydratableInstanceAfterSuspenseInstance =
$$$config.getNextHydratableInstanceAfterSuspenseInstance;
export const commitHydratedContainer = $$$config.commitHydratedContainer;
export const commitHydratedActivityInstance =
$$$config.commitHydratedActivityInstance;
export const commitHydratedSuspenseInstance =
$$$config.commitHydratedSuspenseInstance;
export const clearActivityBoundary = $$$config.clearActivityBoundary;
export const clearSuspenseBoundary = $$$config.clearSuspenseBoundary;
export const clearActivityBoundaryFromContainer =
$$$config.clearActivityBoundaryFromContainer;
export const clearSuspenseBoundaryFromContainer =
$$$config.clearSuspenseBoundaryFromContainer;
export const hideSuspenseBoundary = $$$config.hideSuspenseBoundary;
export const unhideSuspenseBoundary = $$$config.unhideSuspenseBoundary;
export const hideDehydratedBoundary = $$$config.hideDehydratedBoundary;
export const unhideDehydratedBoundary = $$$config.unhideDehydratedBoundary;
export const shouldDeleteUnhydratedTailInstances =
$$$config.shouldDeleteUnhydratedTailInstances;
export const diffHydratedPropsForDevWarnings =