diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 842a33291fb07..39058e9ac5a31 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -7,6 +7,7 @@ * @flow */ +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes'; import type {ElementRef} from 'react'; import type { HostComponent, @@ -301,6 +302,9 @@ export function getChildHostContext( type === 'RCTText' || type === 'RCTVirtualText'; + // TODO: If this is an offscreen host container, we should reuse the + // parent context. + if (prevIsInAParentText !== isInAParentText) { return {isInAParentText}; } else { @@ -413,30 +417,32 @@ export function cloneInstance( }; } -export function cloneHiddenInstance( - instance: Instance, - type: string, - props: Props, - internalInstanceHandle: Object, -): Instance { - const viewConfig = instance.canonical.viewConfig; - const node = instance.node; - const updatePayload = create( - {style: {display: 'none'}}, - viewConfig.validAttributes, - ); - return { - node: cloneNodeWithNewProps(node, updatePayload), - canonical: instance.canonical, - }; +// TODO: These two methods should be replaced with `createOffscreenInstance` and +// `cloneOffscreenInstance`. I did it this way for now because the offscreen +// instance is stored on an extra HostComponent fiber instead of the +// OffscreenComponent fiber, and I didn't want to add an extra check to the +// generic HostComponent path. Instead we should use the OffscreenComponent +// fiber, but currently Fabric expects a 1:1 correspondence between Fabric +// instances and host fibers, so I'm leaving this optimization for later once +// we can confirm this won't break any downstream expectations. +export function getOffscreenContainerType(): string { + return 'RCTView'; } -export function cloneHiddenTextInstance( - instance: Instance, - text: string, - internalInstanceHandle: Object, -): TextInstance { - throw new Error('Not yet implemented.'); +export function getOffscreenContainerProps( + mode: OffscreenMode, + children: ReactNodeList, +): Props { + if (mode === 'hidden') { + return { + children, + style: {display: 'none'}, + }; + } else { + return { + children, + }; + } } export function createContainerChildSet(container: Container): ChildSet { diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 652f32521c249..9bcc1883566d1 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -16,7 +16,7 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; -import type {ReactNodeList} from 'shared/ReactTypes'; +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes'; import type {RootTag} from 'react-reconciler/src/ReactRootTags'; import * as Scheduler from 'scheduler/unstable_mock'; @@ -258,6 +258,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { type: string, rootcontainerInstance: Container, ) { + if (type === 'offscreen') { + return parentHostContext; + } if (type === 'uppercase') { return UPPERCASE_CONTEXT; } @@ -539,47 +542,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { container.children = newChildren; }, - cloneHiddenInstance( - instance: Instance, - type: string, - props: Props, - internalInstanceHandle: Object, - ): Instance { - const clone = cloneInstance( - instance, - null, - type, - props, - props, - internalInstanceHandle, - true, - null, - ); - clone.hidden = true; - return clone; + getOffscreenContainerType(): string { + return 'offscreen'; }, - cloneHiddenTextInstance( - instance: TextInstance, - text: string, - internalInstanceHandle: Object, - ): TextInstance { - const clone = { - text: instance.text, - id: instanceCounter++, - hidden: true, - context: instance.context, + getOffscreenContainerProps( + mode: OffscreenMode, + children: ReactNodeList, + ): Props { + return { + hidden: mode === 'hidden', + children, }; - // Hide from unit tests - Object.defineProperty(clone, 'id', { - value: clone.id, - enumerable: false, - }); - Object.defineProperty(clone, 'context', { - value: clone.context, - enumerable: false, - }); - return clone; }, }; @@ -646,7 +620,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { function getChildren(root) { if (root) { - return root.children; + return useMutation + ? root.children + : removeOffscreenContainersFromChildren(root.children, false); } else { return null; } @@ -654,12 +630,141 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { function getPendingChildren(root) { if (root) { - return root.pendingChildren; + return useMutation + ? root.children + : removeOffscreenContainersFromChildren(root.pendingChildren, false); } else { return null; } } + function removeOffscreenContainersFromChildren(children, hideNearestNode) { + // Mutation mode and persistent mode have different outputs for Offscreen + // and Suspense trees. Persistent mode adds an additional host node wrapper, + // whereas mutation mode does not. + // + // This function removes the offscreen host wrappers so that the output is + // consistent in both modes. That way our tests don't have to fork tree + // assertions based on the renderer mode. + // + // We don't mutate the original tree, but instead return a copy. + // + // This function is only used by our test assertions, via the `getChildren` + // and `getChildrenAsJSX` methods. + let didClone = false; + const newChildren = []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const innerChildren = child.children; + if (innerChildren !== undefined) { + // This is a host instance instance + const instance: Instance = (child: any); + if (instance.type === 'offscreen') { + // This is an offscreen wrapper instance. Remove it from the tree + // and recursively return its children, as if it were a fragment. + didClone = true; + if (instance.text !== null) { + // If this offscreen tree contains only text, we replace it with + // a text child. Related to `shouldReplaceTextContent` feature. + const offscreenTextInstance: TextInstance = { + text: instance.text, + id: instanceCounter++, + hidden: hideNearestNode || instance.hidden, + context: instance.context, + }; + // Hide from unit tests + Object.defineProperty(offscreenTextInstance, 'id', { + value: offscreenTextInstance.id, + enumerable: false, + }); + Object.defineProperty(offscreenTextInstance, 'context', { + value: offscreenTextInstance.context, + enumerable: false, + }); + newChildren.push(offscreenTextInstance); + } else { + // Skip the offscreen node and replace it with its children + const offscreenChildren = removeOffscreenContainersFromChildren( + innerChildren, + hideNearestNode || instance.hidden, + ); + newChildren.push.apply(newChildren, offscreenChildren); + } + } else { + // This is a regular (non-offscreen) instance. If the nearest + // offscreen boundary is hidden, hide this node. + const hidden = hideNearestNode ? true : instance.hidden; + const clonedChildren = removeOffscreenContainersFromChildren( + instance.children, + // We never need to hide the children of this node, since if we're + // inside a hidden tree, then the hidden style will be applied to + // this node. + false, + ); + if ( + clonedChildren === instance.children && + hidden === instance.hidden + ) { + // No changes. Reuse the original instance without cloning. + newChildren.push(instance); + } else { + didClone = true; + const clone: Instance = { + id: instance.id, + type: instance.type, + children: clonedChildren, + text: instance.text, + prop: instance.prop, + hidden: hideNearestNode ? true : instance.hidden, + context: instance.context, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + Object.defineProperty(clone, 'text', { + value: clone.text, + enumerable: false, + }); + Object.defineProperty(clone, 'context', { + value: clone.context, + enumerable: false, + }); + newChildren.push(clone); + } + } + } else { + // This is a text instance + const textInstance: TextInstance = (child: any); + if (hideNearestNode) { + didClone = true; + const clone = { + text: textInstance.text, + id: textInstance.id, + hidden: textInstance.hidden || hideNearestNode, + context: textInstance.context, + }; + Object.defineProperty(clone, 'id', { + value: clone.id, + enumerable: false, + }); + Object.defineProperty(clone, 'context', { + value: clone.context, + enumerable: false, + }); + + newChildren.push(clone); + } else { + newChildren.push(textInstance); + } + } + } + // There are some tests that assume reference equality, so preserve it + // when possible. Alternatively, we could update the tests to compare the + // ids instead. + return didClone ? newChildren : children; + } + function getChildrenAsJSX(root) { const children = childToJSX(getChildren(root), null); if (children === null) { diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index a18514c50c0f2..cbbbaa623385e 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -14,7 +14,7 @@ import type {RootTag} from './ReactRootTags'; import type {WorkTag} from './ReactWorkTags'; import type {TypeOfMode} from './ReactTypeOfMode'; import type {Lanes} from './ReactFiberLane.new'; -import type {SuspenseInstance} from './ReactFiberHostConfig'; +import type {SuspenseInstance, Props} from './ReactFiberHostConfig'; import type {OffscreenProps} from './ReactFiberOffscreenComponent'; import invariant from 'shared/invariant'; @@ -27,6 +27,10 @@ import { enableSyncDefaultUpdates, allowConcurrentByDefault, } from 'shared/ReactFeatureFlags'; +import { + supportsPersistence, + getOffscreenContainerType, +} from './ReactFiberHostConfig'; import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot} from './ReactRootTags'; import { @@ -585,6 +589,25 @@ export function createFiberFromTypeAndProps( return fiber; } +export function createOffscreenHostContainerFiber( + props: Props, + fiberMode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + if (supportsPersistence) { + const type = getOffscreenContainerType(); + const fiber = createFiber(HostComponent, props, key, fiberMode); + fiber.elementType = type; + fiber.type = type; + fiber.lanes = lanes; + return fiber; + } else { + // Only implemented in persistent mode + invariant(false, 'Not implemented.'); + } +} + export function createFiberFromElement( element: ReactElement, mode: TypeOfMode, diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index 4fbbb5a00efca..6d99c6837ed21 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -14,7 +14,7 @@ import type {RootTag} from './ReactRootTags'; import type {WorkTag} from './ReactWorkTags'; import type {TypeOfMode} from './ReactTypeOfMode'; import type {Lanes} from './ReactFiberLane.old'; -import type {SuspenseInstance} from './ReactFiberHostConfig'; +import type {SuspenseInstance, Props} from './ReactFiberHostConfig'; import type {OffscreenProps} from './ReactFiberOffscreenComponent'; import invariant from 'shared/invariant'; @@ -27,6 +27,10 @@ import { enableSyncDefaultUpdates, allowConcurrentByDefault, } from 'shared/ReactFeatureFlags'; +import { + supportsPersistence, + getOffscreenContainerType, +} from './ReactFiberHostConfig'; import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot} from './ReactRootTags'; import { @@ -585,6 +589,25 @@ export function createFiberFromTypeAndProps( return fiber; } +export function createOffscreenHostContainerFiber( + props: Props, + fiberMode: TypeOfMode, + lanes: Lanes, + key: null | string, +): Fiber { + if (supportsPersistence) { + const type = getOffscreenContainerType(); + const fiber = createFiber(HostComponent, props, key, fiberMode); + fiber.elementType = type; + fiber.type = type; + fiber.lanes = lanes; + return fiber; + } else { + // Only implemented in persistent mode + invariant(false, 'Not implemented.'); + } +} + export function createFiberFromElement( element: ReactElement, mode: TypeOfMode, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index adce432d04a79..0b0c763e4fecc 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -142,6 +142,9 @@ import { registerSuspenseInstanceRetry, supportsHydration, isPrimaryRenderer, + supportsMutation, + supportsPersistence, + getOffscreenContainerProps, } from './ReactFiberHostConfig'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import {shouldError, shouldSuspend} from './ReactFiberReconciler'; @@ -199,6 +202,7 @@ import { createFiberFromFragment, createFiberFromOffscreen, createWorkInProgress, + createOffscreenHostContainerFiber, isSimpleFunctionComponent, } from './ReactFiber.new'; import { @@ -224,6 +228,7 @@ import { } from './ReactFiberCacheComponent.new'; import {createCapturedValue} from './ReactCapturedValue'; import {createClassErrorUpdate} from './ReactFiberThrow.new'; +import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new'; import is from 'shared/objectIs'; import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; @@ -728,8 +733,69 @@ function updateOffscreenComponent( workInProgress.updateQueue = spawnedCachePool; } - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; + if (supportsPersistence) { + // In persistent mode, the offscreen children are wrapped in a host node. + // TODO: Optimize this to use the OffscreenComponent fiber instead of + // an extra HostComponent fiber. Need to make sure this doesn't break Fabric + // or some other infra that expects a HostComponent. + const isHidden = + nextProps.mode === 'hidden' && + workInProgress.tag !== LegacyHiddenComponent; + const offscreenContainer = reconcileOffscreenHostContainer( + current, + workInProgress, + isHidden, + nextChildren, + renderLanes, + ); + return offscreenContainer; + } + if (supportsMutation) { + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; + } + return null; +} + +function reconcileOffscreenHostContainer( + currentOffscreen: Fiber | null, + offscreen: Fiber, + isHidden: boolean, + children: any, + renderLanes: Lanes, +) { + const containerProps = getOffscreenContainerProps( + isHidden ? 'hidden' : 'visible', + children, + ); + let hostContainer; + if (currentOffscreen === null) { + hostContainer = createOffscreenHostContainerFiber( + containerProps, + offscreen.mode, + renderLanes, + null, + ); + } else { + const currentHostContainer = currentOffscreen.child; + if (currentHostContainer === null) { + hostContainer = createOffscreenHostContainerFiber( + containerProps, + offscreen.mode, + renderLanes, + null, + ); + hostContainer.flags |= Placement; + } else { + hostContainer = createWorkInProgress( + currentHostContainer, + containerProps, + ); + } + } + hostContainer.return = offscreen; + offscreen.child = hostContainer; + return hostContainer; } // Note: These happen to have identical begin phases, for now. We shouldn't hold @@ -2148,6 +2214,21 @@ function mountSuspenseFallbackChildren( primaryChildFragment.childLanes = NoLanes; primaryChildFragment.pendingProps = primaryChildProps; + if ( + supportsPersistence && + (workInProgress.mode & ConcurrentMode) === NoMode + ) { + const isHidden = true; + const offscreenContainer: Fiber = (primaryChildFragment.child: any); + const containerProps = { + hidden: isHidden, + primaryChildren, + }; + offscreenContainer.pendingProps = containerProps; + offscreenContainer.memoizedProps = containerProps; + completeSuspendedOffscreenHostContainer(null, offscreenContainer); + } + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { // Reset the durations from the first pass so they aren't included in the // final amounts. This seems counterintuitive, since we're intentionally @@ -2290,6 +2371,25 @@ function updateSuspenseFallbackChildren( currentPrimaryChildFragment.treeBaseDuration; } + if (supportsPersistence) { + // In persistent mode, the offscreen children are wrapped in a host node. + // We need to complete it now, because we're going to skip over its normal + // complete phase and go straight to rendering the fallback. + const isHidden = true; + const currentOffscreenContainer = currentPrimaryChildFragment.child; + const offscreenContainer: Fiber = (primaryChildFragment.child: any); + const containerProps = { + hidden: isHidden, + primaryChildren, + }; + offscreenContainer.pendingProps = containerProps; + offscreenContainer.memoizedProps = containerProps; + completeSuspendedOffscreenHostContainer( + currentOffscreenContainer, + offscreenContainer, + ); + } + // The fallback fiber was added as a deletion during the first pass. // However, since we're going to remain on the fallback, we no longer want // to delete it. @@ -2300,6 +2400,28 @@ function updateSuspenseFallbackChildren( primaryChildProps, ); + if (supportsPersistence) { + // In persistent mode, the offscreen children are wrapped in a host node. + // We need to complete it now, because we're going to skip over its normal + // complete phase and go straight to rendering the fallback. + const currentOffscreenContainer = currentPrimaryChildFragment.child; + if (currentOffscreenContainer !== null) { + const isHidden = true; + const offscreenContainer = reconcileOffscreenHostContainer( + currentPrimaryChildFragment, + primaryChildFragment, + isHidden, + primaryChildren, + renderLanes, + ); + offscreenContainer.memoizedProps = offscreenContainer.pendingProps; + completeSuspendedOffscreenHostContainer( + currentOffscreenContainer, + offscreenContainer, + ); + } + } + // Since we're reusing a current tree, we need to reuse the flags, too. // (We don't do this in legacy mode, because in legacy mode we don't re-use // the current tree; see previous branch.) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index df061e0b22583..c54fc312f97a4 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -142,6 +142,9 @@ import { registerSuspenseInstanceRetry, supportsHydration, isPrimaryRenderer, + supportsMutation, + supportsPersistence, + getOffscreenContainerProps, } from './ReactFiberHostConfig'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import {shouldError, shouldSuspend} from './ReactFiberReconciler'; @@ -199,6 +202,7 @@ import { createFiberFromFragment, createFiberFromOffscreen, createWorkInProgress, + createOffscreenHostContainerFiber, isSimpleFunctionComponent, } from './ReactFiber.old'; import { @@ -224,6 +228,7 @@ import { } from './ReactFiberCacheComponent.old'; import {createCapturedValue} from './ReactCapturedValue'; import {createClassErrorUpdate} from './ReactFiberThrow.old'; +import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old'; import is from 'shared/objectIs'; import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; @@ -728,8 +733,69 @@ function updateOffscreenComponent( workInProgress.updateQueue = spawnedCachePool; } - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; + if (supportsPersistence) { + // In persistent mode, the offscreen children are wrapped in a host node. + // TODO: Optimize this to use the OffscreenComponent fiber instead of + // an extra HostComponent fiber. Need to make sure this doesn't break Fabric + // or some other infra that expects a HostComponent. + const isHidden = + nextProps.mode === 'hidden' && + workInProgress.tag !== LegacyHiddenComponent; + const offscreenContainer = reconcileOffscreenHostContainer( + current, + workInProgress, + isHidden, + nextChildren, + renderLanes, + ); + return offscreenContainer; + } + if (supportsMutation) { + reconcileChildren(current, workInProgress, nextChildren, renderLanes); + return workInProgress.child; + } + return null; +} + +function reconcileOffscreenHostContainer( + currentOffscreen: Fiber | null, + offscreen: Fiber, + isHidden: boolean, + children: any, + renderLanes: Lanes, +) { + const containerProps = getOffscreenContainerProps( + isHidden ? 'hidden' : 'visible', + children, + ); + let hostContainer; + if (currentOffscreen === null) { + hostContainer = createOffscreenHostContainerFiber( + containerProps, + offscreen.mode, + renderLanes, + null, + ); + } else { + const currentHostContainer = currentOffscreen.child; + if (currentHostContainer === null) { + hostContainer = createOffscreenHostContainerFiber( + containerProps, + offscreen.mode, + renderLanes, + null, + ); + hostContainer.flags |= Placement; + } else { + hostContainer = createWorkInProgress( + currentHostContainer, + containerProps, + ); + } + } + hostContainer.return = offscreen; + offscreen.child = hostContainer; + return hostContainer; } // Note: These happen to have identical begin phases, for now. We shouldn't hold @@ -2148,6 +2214,21 @@ function mountSuspenseFallbackChildren( primaryChildFragment.childLanes = NoLanes; primaryChildFragment.pendingProps = primaryChildProps; + if ( + supportsPersistence && + (workInProgress.mode & ConcurrentMode) === NoMode + ) { + const isHidden = true; + const offscreenContainer: Fiber = (primaryChildFragment.child: any); + const containerProps = { + hidden: isHidden, + primaryChildren, + }; + offscreenContainer.pendingProps = containerProps; + offscreenContainer.memoizedProps = containerProps; + completeSuspendedOffscreenHostContainer(null, offscreenContainer); + } + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { // Reset the durations from the first pass so they aren't included in the // final amounts. This seems counterintuitive, since we're intentionally @@ -2290,6 +2371,25 @@ function updateSuspenseFallbackChildren( currentPrimaryChildFragment.treeBaseDuration; } + if (supportsPersistence) { + // In persistent mode, the offscreen children are wrapped in a host node. + // We need to complete it now, because we're going to skip over its normal + // complete phase and go straight to rendering the fallback. + const isHidden = true; + const currentOffscreenContainer = currentPrimaryChildFragment.child; + const offscreenContainer: Fiber = (primaryChildFragment.child: any); + const containerProps = { + hidden: isHidden, + primaryChildren, + }; + offscreenContainer.pendingProps = containerProps; + offscreenContainer.memoizedProps = containerProps; + completeSuspendedOffscreenHostContainer( + currentOffscreenContainer, + offscreenContainer, + ); + } + // The fallback fiber was added as a deletion during the first pass. // However, since we're going to remain on the fallback, we no longer want // to delete it. @@ -2300,6 +2400,28 @@ function updateSuspenseFallbackChildren( primaryChildProps, ); + if (supportsPersistence) { + // In persistent mode, the offscreen children are wrapped in a host node. + // We need to complete it now, because we're going to skip over its normal + // complete phase and go straight to rendering the fallback. + const currentOffscreenContainer = currentPrimaryChildFragment.child; + if (currentOffscreenContainer !== null) { + const isHidden = true; + const offscreenContainer = reconcileOffscreenHostContainer( + currentPrimaryChildFragment, + primaryChildFragment, + isHidden, + primaryChildren, + renderLanes, + ); + offscreenContainer.memoizedProps = offscreenContainer.pendingProps; + completeSuspendedOffscreenHostContainer( + currentOffscreenContainer, + offscreenContainer, + ); + } + } + // Since we're reusing a current tree, we need to reuse the flags, too. // (We don't do this in legacy mode, because in legacy mode we don't re-use // the current tree; see previous branch.) diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 9416ac45dd4c5..fea034edaa67f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -84,8 +84,6 @@ import { supportsMutation, supportsPersistence, cloneInstance, - cloneHiddenInstance, - cloneHiddenTextInstance, createContainerChildSet, appendChildToContainerChildSet, finalizeContainerChildren, @@ -200,12 +198,7 @@ let updateHostText; if (supportsMutation) { // Mutation mode - appendAllChildren = function( - parent: Instance, - workInProgress: Fiber, - needsVisibilityToggle: boolean, - isHidden: boolean, - ) { + appendAllChildren = function(parent: Instance, workInProgress: Fiber) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; @@ -293,49 +286,22 @@ if (supportsMutation) { } else if (supportsPersistence) { // Persistent host tree mode - appendAllChildren = function( - parent: Instance, - workInProgress: Fiber, - needsVisibilityToggle: boolean, - isHidden: boolean, - ) { + appendAllChildren = function(parent: Instance, workInProgress: Fiber) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; while (node !== null) { // eslint-disable-next-line no-labels branches: if (node.tag === HostComponent) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const props = node.memoizedProps; - const type = node.type; - instance = cloneHiddenInstance(instance, type, props, node); - } + const instance = node.stateNode; appendInitialChild(parent, instance); } else if (node.tag === HostText) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const text = node.memoizedProps; - instance = cloneHiddenTextInstance(instance, text, node); - } + const instance = node.stateNode; appendInitialChild(parent, instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. - } else if ( - node.tag === OffscreenComponent && - node.memoizedState !== null - ) { - // The children in this boundary are hidden. Toggle their visibility - // before appending. - const child = node.child; - if (child !== null) { - child.return = node; - } - appendAllChildren(parent, node, true, true); } else if (node.child !== null) { node.child.return = node; node = node.child; @@ -361,8 +327,6 @@ if (supportsMutation) { const appendAllChildrenToContainer = function( containerChildSet: ChildSet, workInProgress: Fiber, - needsVisibilityToggle: boolean, - isHidden: boolean, ) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. @@ -370,37 +334,15 @@ if (supportsMutation) { while (node !== null) { // eslint-disable-next-line no-labels branches: if (node.tag === HostComponent) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const props = node.memoizedProps; - const type = node.type; - instance = cloneHiddenInstance(instance, type, props, node); - } + const instance = node.stateNode; appendChildToContainerChildSet(containerChildSet, instance); } else if (node.tag === HostText) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const text = node.memoizedProps; - instance = cloneHiddenTextInstance(instance, text, node); - } + const instance = node.stateNode; appendChildToContainerChildSet(containerChildSet, instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. - } else if ( - node.tag === OffscreenComponent && - node.memoizedState !== null - ) { - // The children in this boundary are hidden. Toggle their visibility - // before appending. - const child = node.child; - if (child !== null) { - child.return = node; - } - appendAllChildrenToContainer(containerChildSet, node, true, true); } else if (node.child !== null) { node.child.return = node; node = node.child; @@ -434,7 +376,7 @@ if (supportsMutation) { const container = portalOrRoot.containerInfo; const newChildSet = createContainerChildSet(container); // If children might have changed, we have to add them all to the set. - appendAllChildrenToContainer(newChildSet, workInProgress, false, false); + appendAllChildrenToContainer(newChildSet, workInProgress); portalOrRoot.pendingChildren = newChildSet; // Schedule an update on the container to swap out the container. markUpdate(workInProgress); @@ -507,7 +449,7 @@ if (supportsMutation) { markUpdate(workInProgress); } else { // If children might have changed, we have to add them all to the set. - appendAllChildren(newInstance, workInProgress, false, false); + appendAllChildren(newInstance, workInProgress); } }; updateHostText = function( @@ -748,6 +690,65 @@ function bubbleProperties(completedWork: Fiber) { return didBailout; } +export function completeSuspendedOffscreenHostContainer( + current: Fiber | null, + workInProgress: Fiber, +) { + // This is a fork of the complete phase for HostComponent. We use it when + // a suspense tree is in its fallback state, because in that case the primary + // tree that includes the offscreen boundary is skipped over without a + // regular complete phase. + // + // We can optimize this path further by inlining the update logic for + // offscreen instances specifically, i.e. skipping the `prepareUpdate` call. + const rootContainerInstance = getRootHostContainer(); + const type = workInProgress.type; + const newProps = workInProgress.memoizedProps; + if (current !== null) { + updateHostComponent( + current, + workInProgress, + type, + newProps, + rootContainerInstance, + ); + } else { + const currentHostContext = getHostContext(); + const instance = createInstance( + type, + newProps, + rootContainerInstance, + currentHostContext, + workInProgress, + ); + + appendAllChildren(instance, workInProgress); + + workInProgress.stateNode = instance; + + // Certain renderers require commit-time effects for initial mount. + // (eg DOM renderer supports auto-focus for certain elements). + // Make sure such renderers get scheduled for later work. + if ( + finalizeInitialChildren( + instance, + type, + newProps, + rootContainerInstance, + currentHostContext, + ) + ) { + markUpdate(workInProgress); + } + + if (workInProgress.ref !== null) { + // If there is a ref on a host node we need to schedule a callback + markRef(workInProgress); + } + } + bubbleProperties(workInProgress); +} + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -868,7 +869,7 @@ function completeWork( workInProgress, ); - appendAllChildren(instance, workInProgress, false, false); + appendAllChildren(instance, workInProgress); workInProgress.stateNode = instance; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 35d17f871ab93..4658938c9d5d4 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -84,8 +84,6 @@ import { supportsMutation, supportsPersistence, cloneInstance, - cloneHiddenInstance, - cloneHiddenTextInstance, createContainerChildSet, appendChildToContainerChildSet, finalizeContainerChildren, @@ -200,12 +198,7 @@ let updateHostText; if (supportsMutation) { // Mutation mode - appendAllChildren = function( - parent: Instance, - workInProgress: Fiber, - needsVisibilityToggle: boolean, - isHidden: boolean, - ) { + appendAllChildren = function(parent: Instance, workInProgress: Fiber) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; @@ -293,49 +286,22 @@ if (supportsMutation) { } else if (supportsPersistence) { // Persistent host tree mode - appendAllChildren = function( - parent: Instance, - workInProgress: Fiber, - needsVisibilityToggle: boolean, - isHidden: boolean, - ) { + appendAllChildren = function(parent: Instance, workInProgress: Fiber) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. let node = workInProgress.child; while (node !== null) { // eslint-disable-next-line no-labels branches: if (node.tag === HostComponent) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const props = node.memoizedProps; - const type = node.type; - instance = cloneHiddenInstance(instance, type, props, node); - } + const instance = node.stateNode; appendInitialChild(parent, instance); } else if (node.tag === HostText) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const text = node.memoizedProps; - instance = cloneHiddenTextInstance(instance, text, node); - } + const instance = node.stateNode; appendInitialChild(parent, instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. - } else if ( - node.tag === OffscreenComponent && - node.memoizedState !== null - ) { - // The children in this boundary are hidden. Toggle their visibility - // before appending. - const child = node.child; - if (child !== null) { - child.return = node; - } - appendAllChildren(parent, node, true, true); } else if (node.child !== null) { node.child.return = node; node = node.child; @@ -361,8 +327,6 @@ if (supportsMutation) { const appendAllChildrenToContainer = function( containerChildSet: ChildSet, workInProgress: Fiber, - needsVisibilityToggle: boolean, - isHidden: boolean, ) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. @@ -370,37 +334,15 @@ if (supportsMutation) { while (node !== null) { // eslint-disable-next-line no-labels branches: if (node.tag === HostComponent) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const props = node.memoizedProps; - const type = node.type; - instance = cloneHiddenInstance(instance, type, props, node); - } + const instance = node.stateNode; appendChildToContainerChildSet(containerChildSet, instance); } else if (node.tag === HostText) { - let instance = node.stateNode; - if (needsVisibilityToggle && isHidden) { - // This child is inside a timed out tree. Hide it. - const text = node.memoizedProps; - instance = cloneHiddenTextInstance(instance, text, node); - } + const instance = node.stateNode; appendChildToContainerChildSet(containerChildSet, instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in // the portal directly. - } else if ( - node.tag === OffscreenComponent && - node.memoizedState !== null - ) { - // The children in this boundary are hidden. Toggle their visibility - // before appending. - const child = node.child; - if (child !== null) { - child.return = node; - } - appendAllChildrenToContainer(containerChildSet, node, true, true); } else if (node.child !== null) { node.child.return = node; node = node.child; @@ -434,7 +376,7 @@ if (supportsMutation) { const container = portalOrRoot.containerInfo; const newChildSet = createContainerChildSet(container); // If children might have changed, we have to add them all to the set. - appendAllChildrenToContainer(newChildSet, workInProgress, false, false); + appendAllChildrenToContainer(newChildSet, workInProgress); portalOrRoot.pendingChildren = newChildSet; // Schedule an update on the container to swap out the container. markUpdate(workInProgress); @@ -507,7 +449,7 @@ if (supportsMutation) { markUpdate(workInProgress); } else { // If children might have changed, we have to add them all to the set. - appendAllChildren(newInstance, workInProgress, false, false); + appendAllChildren(newInstance, workInProgress); } }; updateHostText = function( @@ -748,6 +690,65 @@ function bubbleProperties(completedWork: Fiber) { return didBailout; } +export function completeSuspendedOffscreenHostContainer( + current: Fiber | null, + workInProgress: Fiber, +) { + // This is a fork of the complete phase for HostComponent. We use it when + // a suspense tree is in its fallback state, because in that case the primary + // tree that includes the offscreen boundary is skipped over without a + // regular complete phase. + // + // We can optimize this path further by inlining the update logic for + // offscreen instances specifically, i.e. skipping the `prepareUpdate` call. + const rootContainerInstance = getRootHostContainer(); + const type = workInProgress.type; + const newProps = workInProgress.memoizedProps; + if (current !== null) { + updateHostComponent( + current, + workInProgress, + type, + newProps, + rootContainerInstance, + ); + } else { + const currentHostContext = getHostContext(); + const instance = createInstance( + type, + newProps, + rootContainerInstance, + currentHostContext, + workInProgress, + ); + + appendAllChildren(instance, workInProgress); + + workInProgress.stateNode = instance; + + // Certain renderers require commit-time effects for initial mount. + // (eg DOM renderer supports auto-focus for certain elements). + // Make sure such renderers get scheduled for later work. + if ( + finalizeInitialChildren( + instance, + type, + newProps, + rootContainerInstance, + currentHostContext, + ) + ) { + markUpdate(workInProgress); + } + + if (workInProgress.ref !== null) { + // If there is a ref on a host node we need to schedule a callback + markRef(workInProgress); + } + } + bubbleProperties(workInProgress); +} + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -868,7 +869,7 @@ function completeWork( workInProgress, ); - appendAllChildren(instance, workInProgress, false, false); + appendAllChildren(instance, workInProgress); workInProgress.stateNode = instance; diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoPersistence.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoPersistence.js index d5f84cf43fd6d..2dd44342a399c 100644 --- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoPersistence.js +++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoPersistence.js @@ -28,5 +28,5 @@ export const createContainerChildSet = shim; export const appendChildToContainerChildSet = shim; export const finalizeContainerChildren = shim; export const replaceContainerChildren = shim; -export const cloneHiddenInstance = shim; -export const cloneHiddenTextInstance = shim; +export const getOffscreenContainerType = shim; +export const getOffscreenContainerProps = shim; diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index acae8d206194b..87e1eaa244540 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactNodeList} from 'shared/ReactTypes'; +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes'; import type {Lanes} from './ReactFiberLane.old'; import type {SpawnedCachePool} from './ReactFiberCacheComponent.new'; @@ -18,7 +18,7 @@ export type OffscreenProps = {| // // Default mode is visible. Kind of a weird default for a component // called "Offscreen." Possible alt: ? - mode?: 'hidden' | 'unstable-defer-without-hiding' | 'visible' | null | void, + mode?: OffscreenMode | null | void, children?: ReactNodeList, |}; diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 6a6e9c5e040c3..9dbaa47eb7110 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -127,8 +127,10 @@ export const appendChildToContainerChildSet = export const finalizeContainerChildren = $$$hostConfig.finalizeContainerChildren; export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren; -export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance; -export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance; +export const getOffscreenContainerType = + $$$hostConfig.getOffscreenContainerType; +export const getOffscreenContainerProps = + $$$hostConfig.getOffscreenContainerProps; // ------------------- // Hydration diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 03b70aae07e6d..43f42bddb91d3 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -166,3 +166,8 @@ export interface Thenable<+R> { onReject: (error: mixed) => void | Thenable | U, ): void | Thenable; } + +export type OffscreenMode = + | 'hidden' + | 'unstable-defer-without-hiding' + | 'visible';