diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js
index 842a33291fb07..76559f5c0ffec 100644
--- a/packages/react-native-renderer/src/ReactFabricHostConfig.js
+++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js
@@ -413,30 +413,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 'RTCView';
}
-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..61a9eca97b623 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';
@@ -62,8 +62,8 @@ type TextInstance = {|
|};
type HostContext = Object;
-const NO_CONTEXT = {};
-const UPPERCASE_CONTEXT = {};
+const NO_CONTEXT = {uppercase: false};
+const UPPERCASE_CONTEXT = {uppercase: true};
const UPDATE_SIGNAL = {};
if (__DEV__) {
Object.freeze(NO_CONTEXT);
@@ -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,138 @@ 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 in our test assertions.
+ 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. An alternative would
+ 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';