diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index f395115a4bc2d..f644328ad927e 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,10 +1,11 @@ { - "packages": ["packages/react", "packages/react-dom", "packages/scheduler"], + "packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"], "buildCommand": "download-build-in-codesandbox-ci", "node": "18", "publishDirectory": { "react": "build/oss-experimental/react", "react-dom": "build/oss-experimental/react-dom", + "react-server-dom-webpack": "build/oss-experimental/react-server-dom-webpack", "scheduler": "build/oss-experimental/scheduler" }, "sandboxes": ["new"], diff --git a/package.json b/package.json index 0fcb952cc6f6d..875986ebdaf6a 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "publish-prereleases": "echo 'This command has been deprecated. Please refer to https://github.com/facebook/react/tree/main/scripts/release#trigger-an-automated-prerelease'", "download-build": "node ./scripts/release/download-experimental-build.js", "download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)", - "download-build-in-codesandbox-ci": "yarn build --type=node react/index react-dom/index react-dom/client react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime", + "download-build-in-codesandbox-ci": "yarn build --type=node react/index react.react-server react-dom/index react-dom/client react-dom/src/server react-dom/test-utils react-dom.react-server scheduler/index react/jsx-runtime react/jsx-dev-runtime react-server-dom-webpack", "check-release-dependencies": "node ./scripts/release/check-release-dependencies", "generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js", "flags": "node ./scripts/flags/flags.js" diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js b/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js index a5b070be494e4..7fef110511160 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponentTree.js @@ -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; } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 2d29781dfee97..ad0477232ca02 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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,11 +1146,82 @@ export function clearSuspenseBoundaryFromContainer( } else { parentNode = (container: any); } - clearSuspenseBoundary(parentNode, suspenseInstance); + clearHydrationBoundary(parentNode, hydrationInstance); // Retry if any event replaying was blocked on this. retryIfBlockedOn(container); } +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; + // Unhide all nodes within this suspense boundary. + let depth = 0; + do { + const nextNode = node.nextSibling; + if (node.nodeType === ELEMENT_NODE) { + const instance = ((node: any): HTMLElement & {_stashedDisplay?: string}); + if (isHidden) { + instance._stashedDisplay = instance.style.display; + instance.style.display = 'none'; + } else { + instance.style.display = instance._stashedDisplay || ''; + if (instance.getAttribute('style') === '') { + instance.removeAttribute('style'); + } + } + } else if (node.nodeType === TEXT_NODE) { + const textNode = ((node: any): Text & {_stashedText?: string}); + if (isHidden) { + textNode._stashedText = textNode.nodeValue; + textNode.nodeValue = ''; + } else { + textNode.nodeValue = textNode._stashedText || ''; + } + } + if (nextNode && nextNode.nodeType === COMMENT_NODE) { + const data = ((nextNode: any).data: string); + if (data === SUSPENSE_END_DATA) { + if (depth === 0) { + return; + } else { + depth--; + } + } else if ( + data === SUSPENSE_START_DATA || + data === SUSPENSE_PENDING_START_DATA || + data === SUSPENSE_FALLBACK_START_DATA + ) { + depth++; + } + // TODO: Should we hide preamble contribution in this case? + } + // $FlowFixMe[incompatible-type] we bail out when we get a null + node = nextNode; + } while (node); +} + +export function hideDehydratedBoundary( + suspenseInstance: SuspenseInstance, +): void { + hideOrUnhideDehydratedBoundary(suspenseInstance, true); +} + export function hideInstance(instance: Instance): void { // TODO: Does this work for all element types? What about MathML? Should we // pass host context to this method? @@ -1144,6 +1239,12 @@ export function hideTextInstance(textInstance: TextInstance): void { textInstance.nodeValue = ''; } +export function unhideDehydratedBoundary( + dehydratedInstance: SuspenseInstance | ActivityInstance, +): void { + hideOrUnhideDehydratedBoundary(dehydratedInstance, false); +} + export function unhideInstance(instance: Instance, props: Props): void { instance = ((instance: any): HTMLElement); const styleProp = props[STYLE]; @@ -2986,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; @@ -3000,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 { @@ -3125,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; } } @@ -3169,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 { @@ -3220,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: {}, @@ -3311,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, @@ -3318,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. @@ -3329,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 { @@ -3338,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++; } @@ -3349,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 @@ -3366,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++; } } @@ -3387,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 { diff --git a/packages/react-dom-bindings/src/events/ReactDOMEventListener.js b/packages/react-dom-bindings/src/events/ReactDOMEventListener.js index 243042ba6fde4..139d76ab01ddb 100644 --- a/packages/react-dom-bindings/src/events/ReactDOMEventListener.js +++ b/packages/react-dom-bindings/src/events/ReactDOMEventListener.js @@ -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)) { diff --git a/packages/react-dom-bindings/src/events/ReactDOMEventReplaying.js b/packages/react-dom-bindings/src/events/ReactDOMEventReplaying.js index 52cfb07aaa2d5..a6f6f3055ae70 100644 --- a/packages/react-dom-bindings/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom-bindings/src/events/ReactDOMEventReplaying.js @@ -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 = 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); diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js index 6e47c3b0658e3..cd7ecbbb9b022 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -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 = diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js index f9139094aa9b5..044a547492889 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js @@ -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++; } diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index deef07d6ef663..7e95ad64e7694 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -3986,4 +3986,94 @@ describe('ReactDOMServerPartialHydration', () => { "onRecoverableError: Hydration failed because the server rendered text didn't match the client.", ]); }); + + it('hides a dehydrated suspense boundary if the parent resuspends', async () => { + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => (resolve = resolvePromise)); + const ref = React.createRef(); + + function Child({text}) { + if (suspend) { + throw promise; + } else { + return text; + } + } + + function Sibling({resuspend}) { + if (suspend && resuspend) { + throw promise; + } else { + return null; + } + } + + function Component({text}) { + return ( + + + World + + ); + } + + function App({text, resuspend}) { + const memoized = React.useMemo(() => , [text]); + return ( +
+ + {memoized} + + +
+ ); + } + + suspend = false; + const finalHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + container.innerHTML = finalHTML; + + // On the client we don't have all data yet but we want to start + // hydrating anyway. + suspend = true; + const root = ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([]); + + expect(ref.current).toBe(null); // Still dehydrated + const span = container.getElementsByTagName('span')[0]; + const textNode = span.previousSibling; + expect(textNode.nodeValue).toBe('Hello'); + expect(span.textContent).toBe('World'); + + // Render an update, that resuspends the parent boundary. + // Flushing now now hide the text content. + await act(() => { + root.render(); + }); + + expect(ref.current).toBe(null); + expect(span.style.display).toBe('none'); + expect(textNode.nodeValue).toBe(''); + + // Unsuspending shows the content. + await act(async () => { + suspend = false; + resolve(); + await promise; + }); + + expect(textNode.nodeValue).toBe('Hello'); + expect(span.textContent).toBe('World'); + expect(span.style.display).toBe(''); + expect(ref.current).toBe(span); + }); }); diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 37e994cc7bb71..940f0d4f3b124 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 68a832e72e70e..058ecb6bfd688 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js index 2ca49f677de1f..7873e3ddfa9a2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js @@ -41,8 +41,10 @@ import { insertBefore, insertInContainerBefore, replaceContainerChildren, + hideDehydratedBoundary, hideInstance, hideTextInstance, + unhideDehydratedBoundary, unhideInstance, unhideTextInstance, commitHydratedContainer, @@ -152,6 +154,27 @@ export function commitHostResetTextContent(finishedWork: Fiber) { } } +export function commitShowHideSuspenseBoundary(node: Fiber, isHidden: boolean) { + try { + const instance = node.stateNode; + if (isHidden) { + if (__DEV__) { + runWithFiberInDEV(node, hideDehydratedBoundary, instance); + } else { + hideDehydratedBoundary(instance); + } + } else { + if (__DEV__) { + runWithFiberInDEV(node, unhideDehydratedBoundary, node.stateNode); + } else { + unhideDehydratedBoundary(node.stateNode); + } + } + } catch (error) { + captureCommitPhaseError(node, node.return, error); + } +} + export function commitShowHideHostInstance(node: Fiber, isHidden: boolean) { try { const instance = node.stateNode; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 92e6883c8c652..8065432370add 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -227,6 +227,7 @@ import { commitHostUpdate, commitHostTextUpdate, commitHostResetTextContent, + commitShowHideSuspenseBoundary, commitShowHideHostInstance, commitShowHideHostTextInstance, commitHostPlacement, @@ -1158,6 +1159,10 @@ function hideOrUnhideAllChildren(finishedWork: Fiber, isHidden: boolean) { if (hostSubtreeRoot === null) { commitShowHideHostTextInstance(node, isHidden); } + } else if (node.tag === DehydratedFragment) { + if (hostSubtreeRoot === null) { + commitShowHideSuspenseBoundary(node, isHidden); + } } else if ( (node.tag === OffscreenComponent || node.tag === LegacyHiddenComponent) && diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 2837b8939aa79..9ddfca471e2d7 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -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; diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js index 0bb85246dfe24..9b907b673f892 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js @@ -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,19 +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 hideDehydratedBoundary = shim; +export const unhideDehydratedBoundary = shim; export const shouldDeleteUnhydratedTailInstances = shim; export const diffHydratedPropsForDevWarnings = shim; export const diffHydratedTextForDevWarnings = shim; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index daa9c8ca7a52a..f9e7580e09bed 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js index a790bede9055b..51df2a25d6c53 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js +++ b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberSuspenseContext.js b/packages/react-reconciler/src/ReactFiberSuspenseContext.js index 7bc365e84a383..8f712b1b106fc 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseContext.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseContext.js @@ -11,7 +11,6 @@ import type {SuspenseProps} from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; -import type {OffscreenState} from './ReactFiberOffscreenComponent'; import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags'; import {createCursor, push, pop} from './ReactFiberStack'; @@ -115,19 +114,10 @@ export function pushOffscreenSuspenseHandler(fiber: Fiber): void { // into separate functions for Suspense and Offscreen. pushSuspenseListContext(fiber, suspenseStackCursor.current); push(suspenseHandlerStackCursor, fiber, fiber); - if (shellBoundary !== null) { - // A parent boundary is showing a fallback, so we've already rendered - // deeper than the shell. - } else { - const current = fiber.alternate; - if (current !== null) { - const prevState: OffscreenState = current.memoizedState; - if (prevState !== null) { - // This is the first boundary in the stack that's already showing - // a fallback. So everything outside is considered the shell. - shellBoundary = fiber; - } - } + if (shellBoundary === null) { + // We're rendering hidden content. If it suspends, we can handle it by + // just not committing the offscreen boundary. + shellBoundary = fiber; } } else { // This is a LegacyHidden component. diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index 2e2466a4f5a83..9699e5897f797 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -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) diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index f8e06456eeecf..d083d189b3e5d 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -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, ... }; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index f890a78a80e71..4e3fb62b09912 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -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,24 +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 hideDehydratedBoundary = $$$config.hideDehydratedBoundary; +export const unhideDehydratedBoundary = $$$config.unhideDehydratedBoundary; export const shouldDeleteUnhydratedTailInstances = $$$config.shouldDeleteUnhydratedTailInstances; export const diffHydratedPropsForDevWarnings =