@samitouri / QOS-React / commits / 392253a774

[Fabric] Use container node to toggle the visibility of Offscreen and Suspense trees (#21960)

* Fix type of Offscreen props argument Fixes an oversight from a previous refactor. The fiber that wraps a Suspense component's children used to be a Fragment but now it's on Offscreen fiber, so its props type has changed. There's a special hydration path where I forgot to update this. This isn't observable because we don't ever end up rendering this particular fiber (because the Suspense boundary is in its fallback state) but we should fix it anyway to avoid a potential regression in the future. * Extract createOffscreenFromFiber logic ...into a new method called `createWorkInProgressOffscreenFiber`. Just for symmetry with `updateWorkInProgressOffscreenFiber`. Doesn't change any behavior. * [Fabric] Use container node to hide/show tree This changes how we hide and show the contents of Offscreen boundaries in the React Fabric renderer (persistent mode), and also Suspense boundaries which use the same feature.= The way it used to work was that when a boundary is hidden, in the complete phase, instead of calling the normal `cloneInstance` method inside `appendAllChildren`, we would call a forked method called `cloneHiddenInstance` for each of the nearest host nodes within the subtree. This design was largely based on how it works in React DOM (mutation mode), where instead of cloning the nearest host nodes, we mutate their `style.display` property. The motivation for doing it this way in React DOM was because there's no built-in browser API for hiding a collection of DOM nodes without affecting their layout. In Fabric, however, there is no such limitation, so we can instead wrap in an extra host node and apply a hidden style. The immediate motivation for this change is that Fabric on Android has a view pooling mechanism for instances that relies on the assumption that a current Fiber that is cloned and replaced by a new Fiber will never appear in a future commit. When this assumption is broken, it may cause crashes. In the current implementation, that can indeed happen when a node that was previously hidden is toggled back to visible. Although this change sidesteps the issue, we may introduce in other features in the future that would benefit from being able to revert back to an older node without cloning it again, such as animations. The way I've implemented this is to insert an additional HostComponent fiber as the child of each OffscreenComponent. The extra fiber is not ideal — the way I'd prefer to do it is to attach the host instance to the OffscreenComponent. However, the native Fabric implementation currently expects a 1:1 correspondence between HostComponents and host instances, so I've deferred that optimization to a future PR to derisk fixing the Fabric pooling crash. I left a TODO in the host config with a description of the remaining steps, but this alone should be sufficient to unblock.

Andrew Clark committed Jul 26, 2021 at 16:17 UTC 392253a774f33c0b8e2e25757ca6d6aa0ab48fce
12 files changed +684 -239
packages/react-native-renderer/src/ReactFabricHostConfig.js
+28 -22
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes';
11 import type {ElementRef} from 'react';
12 import type {
13 HostComponent,
@@ -301,6 +302,9 @@ export function getChildHostContext(
302 type === 'RCTText' ||
303 type === 'RCTVirtualText';
304
305 + // TODO: If this is an offscreen host container, we should reuse the
306 + // parent context.
307 +
308 if (prevIsInAParentText !== isInAParentText) {
309 return {isInAParentText};
310 } else {
@@ -413,30 +417,32 @@ export function cloneInstance(
417 };
418 }
419
416 -export function cloneHiddenInstance(
417 - instance: Instance,
418 - type: string,
419 - props: Props,
420 - internalInstanceHandle: Object,
421 -): Instance {
422 - const viewConfig = instance.canonical.viewConfig;
423 - const node = instance.node;
424 - const updatePayload = create(
425 - {style: {display: 'none'}},
426 - viewConfig.validAttributes,
427 - );
428 - return {
429 - node: cloneNodeWithNewProps(node, updatePayload),
430 - canonical: instance.canonical,
431 - };
420 +// TODO: These two methods should be replaced with `createOffscreenInstance` and
421 +// `cloneOffscreenInstance`. I did it this way for now because the offscreen
422 +// instance is stored on an extra HostComponent fiber instead of the
423 +// OffscreenComponent fiber, and I didn't want to add an extra check to the
424 +// generic HostComponent path. Instead we should use the OffscreenComponent
425 +// fiber, but currently Fabric expects a 1:1 correspondence between Fabric
426 +// instances and host fibers, so I'm leaving this optimization for later once
427 +// we can confirm this won't break any downstream expectations.
428 +export function getOffscreenContainerType(): string {
429 + return 'RCTView';
430 }
431
434 -export function cloneHiddenTextInstance(
435 - instance: Instance,
436 - text: string,
437 - internalInstanceHandle: Object,
438 -): TextInstance {
439 - throw new Error('Not yet implemented.');
432 +export function getOffscreenContainerProps(
433 + mode: OffscreenMode,
434 + children: ReactNodeList,
435 +): Props {
436 + if (mode === 'hidden') {
437 + return {
438 + children,
439 + style: {display: 'none'},
440 + };
441 + } else {
442 + return {
443 + children,
444 + };
445 + }
446 }
447
448 export function createContainerChildSet(container: Container): ChildSet {
packages/react-noop-renderer/src/createReactNoop.js
+159 -41
@@ -16,7 +16,7 @@
16
17 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
18 import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
19 -import type {ReactNodeList} from 'shared/ReactTypes';
19 +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes';
20 import type {RootTag} from 'react-reconciler/src/ReactRootTags';
21
22 import * as Scheduler from 'scheduler/unstable_mock';
@@ -258,6 +258,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
258 type: string,
259 rootcontainerInstance: Container,
260 ) {
261 + if (type === 'offscreen') {
262 + return parentHostContext;
263 + }
264 if (type === 'uppercase') {
265 return UPPERCASE_CONTEXT;
266 }
@@ -539,47 +542,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
542 container.children = newChildren;
543 },
544
542 - cloneHiddenInstance(
543 - instance: Instance,
544 - type: string,
545 - props: Props,
546 - internalInstanceHandle: Object,
547 - ): Instance {
548 - const clone = cloneInstance(
549 - instance,
550 - null,
551 - type,
552 - props,
553 - props,
554 - internalInstanceHandle,
555 - true,
556 - null,
557 - );
558 - clone.hidden = true;
559 - return clone;
545 + getOffscreenContainerType(): string {
546 + return 'offscreen';
547 },
548
562 - cloneHiddenTextInstance(
563 - instance: TextInstance,
564 - text: string,
565 - internalInstanceHandle: Object,
566 - ): TextInstance {
567 - const clone = {
568 - text: instance.text,
569 - id: instanceCounter++,
570 - hidden: true,
571 - context: instance.context,
549 + getOffscreenContainerProps(
550 + mode: OffscreenMode,
551 + children: ReactNodeList,
552 + ): Props {
553 + return {
554 + hidden: mode === 'hidden',
555 + children,
556 };
573 - // Hide from unit tests
574 - Object.defineProperty(clone, 'id', {
575 - value: clone.id,
576 - enumerable: false,
577 - });
578 - Object.defineProperty(clone, 'context', {
579 - value: clone.context,
580 - enumerable: false,
581 - });
582 - return clone;
557 },
558 };
559
@@ -646,7 +620,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
620
621 function getChildren(root) {
622 if (root) {
649 - return root.children;
623 + return useMutation
624 + ? root.children
625 + : removeOffscreenContainersFromChildren(root.children, false);
626 } else {
627 return null;
628 }
@@ -654,12 +630,154 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
630
631 function getPendingChildren(root) {
632 if (root) {
657 - return root.pendingChildren;
633 + return useMutation
634 + ? root.children
635 + : removeOffscreenContainersFromChildren(root.pendingChildren, false);
636 } else {
637 return null;
638 }
639 }
640
641 + function removeOffscreenContainersFromChildren(children, hideNearestNode) {
642 + // Mutation mode and persistent mode have different outputs for Offscreen
643 + // and Suspense trees. Persistent mode adds an additional host node wrapper,
644 + // whereas mutation mode does not.
645 + //
646 + // This function removes the offscreen host wrappers so that the output is
647 + // consistent. If the offscreen node is hidden, it transfers the hiddenness
648 + // to the child nodes, to mimic how it works in mutation mode. That way our
649 + // tests don't have to fork tree assertions.
650 + //
651 + // So, it takes a tree that looks like this:
652 + //
653 + // <offscreen hidden={true}>
654 + // <span>A</span>
655 + // <span>B</span>
656 + // </offscren>
657 + //
658 + // And turns it into this:
659 + //
660 + // <span hidden={true}>A</span>
661 + // <span hidden={true}>B</span>
662 + //
663 + // We don't mutate the original tree, but instead return a copy.
664 + //
665 + // This function is only used by our test assertions, via the `getChildren`
666 + // and `getChildrenAsJSX` methods.
667 + let didClone = false;
668 + const newChildren = [];
669 + for (let i = 0; i < children.length; i++) {
670 + const child = children[i];
671 + const innerChildren = child.children;
672 + if (innerChildren !== undefined) {
673 + // This is a host instance instance
674 + const instance: Instance = (child: any);
675 + if (instance.type === 'offscreen') {
676 + // This is an offscreen wrapper instance. Remove it from the tree
677 + // and recursively return its children, as if it were a fragment.
678 + didClone = true;
679 + if (instance.text !== null) {
680 + // If this offscreen tree contains only text, we replace it with
681 + // a text child. Related to `shouldReplaceTextContent` feature.
682 + const offscreenTextInstance: TextInstance = {
683 + text: instance.text,
684 + id: instanceCounter++,
685 + hidden: hideNearestNode || instance.hidden,
686 + context: instance.context,
687 + };
688 + // Hide from unit tests
689 + Object.defineProperty(offscreenTextInstance, 'id', {
690 + value: offscreenTextInstance.id,
691 + enumerable: false,
692 + });
693 + Object.defineProperty(offscreenTextInstance, 'context', {
694 + value: offscreenTextInstance.context,
695 + enumerable: false,
696 + });
697 + newChildren.push(offscreenTextInstance);
698 + } else {
699 + // Skip the offscreen node and replace it with its children
700 + const offscreenChildren = removeOffscreenContainersFromChildren(
701 + innerChildren,
702 + hideNearestNode || instance.hidden,
703 + );
704 + newChildren.push.apply(newChildren, offscreenChildren);
705 + }
706 + } else {
707 + // This is a regular (non-offscreen) instance. If the nearest
708 + // offscreen boundary is hidden, hide this node.
709 + const hidden = hideNearestNode ? true : instance.hidden;
710 + const clonedChildren = removeOffscreenContainersFromChildren(
711 + instance.children,
712 + // We never need to hide the children of this node, since if we're
713 + // inside a hidden tree, then the hidden style will be applied to
714 + // this node.
715 + false,
716 + );
717 + if (
718 + clonedChildren === instance.children &&
719 + hidden === instance.hidden
720 + ) {
721 + // No changes. Reuse the original instance without cloning.
722 + newChildren.push(instance);
723 + } else {
724 + didClone = true;
725 + const clone: Instance = {
726 + id: instance.id,
727 + type: instance.type,
728 + children: clonedChildren,
729 + text: instance.text,
730 + prop: instance.prop,
731 + hidden: hideNearestNode ? true : instance.hidden,
732 + context: instance.context,
733 + };
734 + Object.defineProperty(clone, 'id', {
735 + value: clone.id,
736 + enumerable: false,
737 + });
738 + Object.defineProperty(clone, 'text', {
739 + value: clone.text,
740 + enumerable: false,
741 + });
742 + Object.defineProperty(clone, 'context', {
743 + value: clone.context,
744 + enumerable: false,
745 + });
746 + newChildren.push(clone);
747 + }
748 + }
749 + } else {
750 + // This is a text instance
751 + const textInstance: TextInstance = (child: any);
752 + if (hideNearestNode) {
753 + didClone = true;
754 + const clone = {
755 + text: textInstance.text,
756 + id: textInstance.id,
757 + hidden: textInstance.hidden || hideNearestNode,
758 + context: textInstance.context,
759 + };
760 + Object.defineProperty(clone, 'id', {
761 + value: clone.id,
762 + enumerable: false,
763 + });
764 + Object.defineProperty(clone, 'context', {
765 + value: clone.context,
766 + enumerable: false,
767 + });
768 +
769 + newChildren.push(clone);
770 + } else {
771 + newChildren.push(textInstance);
772 + }
773 + }
774 + }
775 + // There are some tests that assume reference equality, so preserve it
776 + // when possible. Alternatively, we could update the tests to compare the
777 + // ids instead.
778 + return didClone ? newChildren : children;
779 + }
780 +
781 function getChildrenAsJSX(root) {
782 const children = childToJSX(getChildren(root), null);
783 if (children === null) {
packages/react-reconciler/src/ReactFiber.new.js
+24 -1
@@ -14,7 +14,7 @@ import type {RootTag} from './ReactRootTags';
14 import type {WorkTag} from './ReactWorkTags';
15 import type {TypeOfMode} from './ReactTypeOfMode';
16 import type {Lanes} from './ReactFiberLane.new';
17 -import type {SuspenseInstance} from './ReactFiberHostConfig';
17 +import type {SuspenseInstance, Props} from './ReactFiberHostConfig';
18 import type {OffscreenProps} from './ReactFiberOffscreenComponent';
19
20 import invariant from 'shared/invariant';
@@ -27,6 +27,10 @@ import {
27 enableSyncDefaultUpdates,
28 allowConcurrentByDefault,
29 } from 'shared/ReactFeatureFlags';
30 +import {
31 + supportsPersistence,
32 + getOffscreenContainerType,
33 +} from './ReactFiberHostConfig';
34 import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
35 import {ConcurrentRoot} from './ReactRootTags';
36 import {
@@ -585,6 +589,25 @@ export function createFiberFromTypeAndProps(
589 return fiber;
590 }
591
592 +export function createOffscreenHostContainerFiber(
593 + props: Props,
594 + fiberMode: TypeOfMode,
595 + lanes: Lanes,
596 + key: null | string,
597 +): Fiber {
598 + if (supportsPersistence) {
599 + const type = getOffscreenContainerType();
600 + const fiber = createFiber(HostComponent, props, key, fiberMode);
601 + fiber.elementType = type;
602 + fiber.type = type;
603 + fiber.lanes = lanes;
604 + return fiber;
605 + } else {
606 + // Only implemented in persistent mode
607 + invariant(false, 'Not implemented.');
608 + }
609 +}
610 +
611 export function createFiberFromElement(
612 element: ReactElement,
613 mode: TypeOfMode,
packages/react-reconciler/src/ReactFiber.old.js
+24 -1
@@ -14,7 +14,7 @@ import type {RootTag} from './ReactRootTags';
14 import type {WorkTag} from './ReactWorkTags';
15 import type {TypeOfMode} from './ReactTypeOfMode';
16 import type {Lanes} from './ReactFiberLane.old';
17 -import type {SuspenseInstance} from './ReactFiberHostConfig';
17 +import type {SuspenseInstance, Props} from './ReactFiberHostConfig';
18 import type {OffscreenProps} from './ReactFiberOffscreenComponent';
19
20 import invariant from 'shared/invariant';
@@ -27,6 +27,10 @@ import {
27 enableSyncDefaultUpdates,
28 allowConcurrentByDefault,
29 } from 'shared/ReactFeatureFlags';
30 +import {
31 + supportsPersistence,
32 + getOffscreenContainerType,
33 +} from './ReactFiberHostConfig';
34 import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
35 import {ConcurrentRoot} from './ReactRootTags';
36 import {
@@ -585,6 +589,25 @@ export function createFiberFromTypeAndProps(
589 return fiber;
590 }
591
592 +export function createOffscreenHostContainerFiber(
593 + props: Props,
594 + fiberMode: TypeOfMode,
595 + lanes: Lanes,
596 + key: null | string,
597 +): Fiber {
598 + if (supportsPersistence) {
599 + const type = getOffscreenContainerType();
600 + const fiber = createFiber(HostComponent, props, key, fiberMode);
601 + fiber.elementType = type;
602 + fiber.type = type;
603 + fiber.lanes = lanes;
604 + return fiber;
605 + } else {
606 + // Only implemented in persistent mode
607 + invariant(false, 'Not implemented.');
608 + }
609 +}
610 +
611 export function createFiberFromElement(
612 element: ReactElement,
613 mode: TypeOfMode,
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+150 -17
@@ -9,8 +9,8 @@
9
10 import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
11 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
12 -import type {Fiber} from './ReactInternalTypes';
13 -import type {FiberRoot} from './ReactInternalTypes';
12 +import type {Fiber, FiberRoot} from './ReactInternalTypes';
13 +import type {TypeOfMode} from './ReactTypeOfMode';
14 import type {Lanes, Lane} from './ReactFiberLane.new';
15 import type {MutableSource} from 'shared/ReactTypes';
16 import type {
@@ -142,6 +142,9 @@ import {
142 registerSuspenseInstanceRetry,
143 supportsHydration,
144 isPrimaryRenderer,
145 + supportsMutation,
146 + supportsPersistence,
147 + getOffscreenContainerProps,
148 } from './ReactFiberHostConfig';
149 import type {SuspenseInstance} from './ReactFiberHostConfig';
150 import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@@ -199,6 +202,7 @@ import {
202 createFiberFromFragment,
203 createFiberFromOffscreen,
204 createWorkInProgress,
205 + createOffscreenHostContainerFiber,
206 isSimpleFunctionComponent,
207 } from './ReactFiber.new';
208 import {
@@ -224,6 +228,7 @@ import {
228 } from './ReactFiberCacheComponent.new';
229 import {createCapturedValue} from './ReactCapturedValue';
230 import {createClassErrorUpdate} from './ReactFiberThrow.new';
231 +import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
232 import is from 'shared/objectIs';
233
234 import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -728,8 +733,69 @@ function updateOffscreenComponent(
733 workInProgress.updateQueue = spawnedCachePool;
734 }
735
731 - reconcileChildren(current, workInProgress, nextChildren, renderLanes);
732 - return workInProgress.child;
736 + if (supportsPersistence) {
737 + // In persistent mode, the offscreen children are wrapped in a host node.
738 + // TODO: Optimize this to use the OffscreenComponent fiber instead of
739 + // an extra HostComponent fiber. Need to make sure this doesn't break Fabric
740 + // or some other infra that expects a HostComponent.
741 + const isHidden =
742 + nextProps.mode === 'hidden' &&
743 + workInProgress.tag !== LegacyHiddenComponent;
744 + const offscreenContainer = reconcileOffscreenHostContainer(
745 + current,
746 + workInProgress,
747 + isHidden,
748 + nextChildren,
749 + renderLanes,
750 + );
751 + return offscreenContainer;
752 + }
753 + if (supportsMutation) {
754 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
755 + return workInProgress.child;
756 + }
757 + return null;
758 +}
759 +
760 +function reconcileOffscreenHostContainer(
761 + currentOffscreen: Fiber | null,
762 + offscreen: Fiber,
763 + isHidden: boolean,
764 + children: any,
765 + renderLanes: Lanes,
766 +) {
767 + const containerProps = getOffscreenContainerProps(
768 + isHidden ? 'hidden' : 'visible',
769 + children,
770 + );
771 + let hostContainer;
772 + if (currentOffscreen === null) {
773 + hostContainer = createOffscreenHostContainerFiber(
774 + containerProps,
775 + offscreen.mode,
776 + renderLanes,
777 + null,
778 + );
779 + } else {
780 + const currentHostContainer = currentOffscreen.child;
781 + if (currentHostContainer === null) {
782 + hostContainer = createOffscreenHostContainerFiber(
783 + containerProps,
784 + offscreen.mode,
785 + renderLanes,
786 + null,
787 + );
788 + hostContainer.flags |= Placement;
789 + } else {
790 + hostContainer = createWorkInProgress(
791 + currentHostContainer,
792 + containerProps,
793 + );
794 + }
795 + }
796 + hostContainer.return = offscreen;
797 + offscreen.child = hostContainer;
798 + return hostContainer;
799 }
800
801 // Note: These happen to have identical begin phases, for now. We shouldn't hold
@@ -2112,11 +2178,10 @@ function mountSuspensePrimaryChildren(
2178 mode: 'visible',
2179 children: primaryChildren,
2180 };
2115 - const primaryChildFragment = createFiberFromOffscreen(
2181 + const primaryChildFragment = mountWorkInProgressOffscreenFiber(
2182 primaryChildProps,
2183 mode,
2184 renderLanes,
2119 - null,
2185 );
2186 primaryChildFragment.return = workInProgress;
2187 workInProgress.child = primaryChildFragment;
@@ -2149,6 +2214,21 @@ function mountSuspenseFallbackChildren(
2214 primaryChildFragment.childLanes = NoLanes;
2215 primaryChildFragment.pendingProps = primaryChildProps;
2216
2217 + if (
2218 + supportsPersistence &&
2219 + (workInProgress.mode & ConcurrentMode) === NoMode
2220 + ) {
2221 + const isHidden = true;
2222 + const offscreenContainer: Fiber = (primaryChildFragment.child: any);
2223 + const containerProps = {
2224 + hidden: isHidden,
2225 + primaryChildren,
2226 + };
2227 + offscreenContainer.pendingProps = containerProps;
2228 + offscreenContainer.memoizedProps = containerProps;
2229 + completeSuspendedOffscreenHostContainer(null, offscreenContainer);
2230 + }
2231 +
2232 if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2233 // Reset the durations from the first pass so they aren't included in the
2234 // final amounts. This seems counterintuitive, since we're intentionally
@@ -2167,11 +2247,10 @@ function mountSuspenseFallbackChildren(
2247 null,
2248 );
2249 } else {
2170 - primaryChildFragment = createFiberFromOffscreen(
2250 + primaryChildFragment = mountWorkInProgressOffscreenFiber(
2251 primaryChildProps,
2252 mode,
2253 NoLanes,
2174 - null,
2254 );
2255 fallbackChildFragment = createFiberFromFragment(
2256 fallbackChildren,
@@ -2188,7 +2267,17 @@ function mountSuspenseFallbackChildren(
2267 return fallbackChildFragment;
2268 }
2269
2191 -function createWorkInProgressOffscreenFiber(
2270 +function mountWorkInProgressOffscreenFiber(
2271 + offscreenProps: OffscreenProps,
2272 + mode: TypeOfMode,
2273 + renderLanes: Lanes,
2274 +) {
2275 + // The props argument to `createFiberFromOffscreen` is `any` typed, so we use
2276 + // this wrapper function to constrain it.
2277 + return createFiberFromOffscreen(offscreenProps, mode, NoLanes, null);
2278 +}
2279 +
2280 +function updateWorkInProgressOffscreenFiber(
2281 current: Fiber,
2282 offscreenProps: OffscreenProps,
2283 ) {
@@ -2207,7 +2296,7 @@ function updateSuspensePrimaryChildren(
2296 const currentFallbackChildFragment: Fiber | null =
2297 currentPrimaryChildFragment.sibling;
2298
2210 - const primaryChildFragment = createWorkInProgressOffscreenFiber(
2299 + const primaryChildFragment = updateWorkInProgressOffscreenFiber(
2300 currentPrimaryChildFragment,
2301 {
2302 mode: 'visible',
@@ -2282,16 +2371,57 @@ function updateSuspenseFallbackChildren(
2371 currentPrimaryChildFragment.treeBaseDuration;
2372 }
2373
2374 + if (supportsPersistence) {
2375 + // In persistent mode, the offscreen children are wrapped in a host node.
2376 + // We need to complete it now, because we're going to skip over its normal
2377 + // complete phase and go straight to rendering the fallback.
2378 + const isHidden = true;
2379 + const currentOffscreenContainer = currentPrimaryChildFragment.child;
2380 + const offscreenContainer: Fiber = (primaryChildFragment.child: any);
2381 + const containerProps = {
2382 + hidden: isHidden,
2383 + primaryChildren,
2384 + };
2385 + offscreenContainer.pendingProps = containerProps;
2386 + offscreenContainer.memoizedProps = containerProps;
2387 + completeSuspendedOffscreenHostContainer(
2388 + currentOffscreenContainer,
2389 + offscreenContainer,
2390 + );
2391 + }
2392 +
2393 // The fallback fiber was added as a deletion during the first pass.
2394 // However, since we're going to remain on the fallback, we no longer want
2395 // to delete it.
2396 workInProgress.deletions = null;
2397 } else {
2290 - primaryChildFragment = createWorkInProgressOffscreenFiber(
2398 + primaryChildFragment = updateWorkInProgressOffscreenFiber(
2399 currentPrimaryChildFragment,
2400 primaryChildProps,
2401 );
2402
2403 + if (supportsPersistence) {
2404 + // In persistent mode, the offscreen children are wrapped in a host node.
2405 + // We need to complete it now, because we're going to skip over its normal
2406 + // complete phase and go straight to rendering the fallback.
2407 + const currentOffscreenContainer = currentPrimaryChildFragment.child;
2408 + if (currentOffscreenContainer !== null) {
2409 + const isHidden = true;
2410 + const offscreenContainer = reconcileOffscreenHostContainer(
2411 + currentPrimaryChildFragment,
2412 + primaryChildFragment,
2413 + isHidden,
2414 + primaryChildren,
2415 + renderLanes,
2416 + );
2417 + offscreenContainer.memoizedProps = offscreenContainer.pendingProps;
2418 + completeSuspendedOffscreenHostContainer(
2419 + currentOffscreenContainer,
2420 + offscreenContainer,
2421 + );
2422 + }
2423 + }
2424 +
2425 // Since we're reusing a current tree, we need to reuse the flags, too.
2426 // (We don't do this in legacy mode, because in legacy mode we don't re-use
2427 // the current tree; see previous branch.)
@@ -2355,16 +2485,19 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating(
2485 fallbackChildren,
2486 renderLanes,
2487 ) {
2358 - const mode = workInProgress.mode;
2359 - const primaryChildFragment = createFiberFromOffscreen(
2360 - primaryChildren,
2361 - mode,
2488 + const fiberMode = workInProgress.mode;
2489 + const primaryChildProps: OffscreenProps = {
2490 + mode: 'visible',
2491 + children: primaryChildren,
2492 + };
2493 + const primaryChildFragment = mountWorkInProgressOffscreenFiber(
2494 + primaryChildProps,
2495 + fiberMode,
2496 NoLanes,
2363 - null,
2497 );
2498 const fallbackChildFragment = createFiberFromFragment(
2499 fallbackChildren,
2367 - mode,
2500 + fiberMode,
2501 renderLanes,
2502 null,
2503 );
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+150 -17
@@ -9,8 +9,8 @@
9
10 import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
11 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
12 -import type {Fiber} from './ReactInternalTypes';
13 -import type {FiberRoot} from './ReactInternalTypes';
12 +import type {Fiber, FiberRoot} from './ReactInternalTypes';
13 +import type {TypeOfMode} from './ReactTypeOfMode';
14 import type {Lanes, Lane} from './ReactFiberLane.old';
15 import type {MutableSource} from 'shared/ReactTypes';
16 import type {
@@ -142,6 +142,9 @@ import {
142 registerSuspenseInstanceRetry,
143 supportsHydration,
144 isPrimaryRenderer,
145 + supportsMutation,
146 + supportsPersistence,
147 + getOffscreenContainerProps,
148 } from './ReactFiberHostConfig';
149 import type {SuspenseInstance} from './ReactFiberHostConfig';
150 import {shouldError, shouldSuspend} from './ReactFiberReconciler';
@@ -199,6 +202,7 @@ import {
202 createFiberFromFragment,
203 createFiberFromOffscreen,
204 createWorkInProgress,
205 + createOffscreenHostContainerFiber,
206 isSimpleFunctionComponent,
207 } from './ReactFiber.old';
208 import {
@@ -224,6 +228,7 @@ import {
228 } from './ReactFiberCacheComponent.old';
229 import {createCapturedValue} from './ReactCapturedValue';
230 import {createClassErrorUpdate} from './ReactFiberThrow.old';
231 +import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
232 import is from 'shared/objectIs';
233
234 import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -728,8 +733,69 @@ function updateOffscreenComponent(
733 workInProgress.updateQueue = spawnedCachePool;
734 }
735
731 - reconcileChildren(current, workInProgress, nextChildren, renderLanes);
732 - return workInProgress.child;
736 + if (supportsPersistence) {
737 + // In persistent mode, the offscreen children are wrapped in a host node.
738 + // TODO: Optimize this to use the OffscreenComponent fiber instead of
739 + // an extra HostComponent fiber. Need to make sure this doesn't break Fabric
740 + // or some other infra that expects a HostComponent.
741 + const isHidden =
742 + nextProps.mode === 'hidden' &&
743 + workInProgress.tag !== LegacyHiddenComponent;
744 + const offscreenContainer = reconcileOffscreenHostContainer(
745 + current,
746 + workInProgress,
747 + isHidden,
748 + nextChildren,
749 + renderLanes,
750 + );
751 + return offscreenContainer;
752 + }
753 + if (supportsMutation) {
754 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
755 + return workInProgress.child;
756 + }
757 + return null;
758 +}
759 +
760 +function reconcileOffscreenHostContainer(
761 + currentOffscreen: Fiber | null,
762 + offscreen: Fiber,
763 + isHidden: boolean,
764 + children: any,
765 + renderLanes: Lanes,
766 +) {
767 + const containerProps = getOffscreenContainerProps(
768 + isHidden ? 'hidden' : 'visible',
769 + children,
770 + );
771 + let hostContainer;
772 + if (currentOffscreen === null) {
773 + hostContainer = createOffscreenHostContainerFiber(
774 + containerProps,
775 + offscreen.mode,
776 + renderLanes,
777 + null,
778 + );
779 + } else {
780 + const currentHostContainer = currentOffscreen.child;
781 + if (currentHostContainer === null) {
782 + hostContainer = createOffscreenHostContainerFiber(
783 + containerProps,
784 + offscreen.mode,
785 + renderLanes,
786 + null,
787 + );
788 + hostContainer.flags |= Placement;
789 + } else {
790 + hostContainer = createWorkInProgress(
791 + currentHostContainer,
792 + containerProps,
793 + );
794 + }
795 + }
796 + hostContainer.return = offscreen;
797 + offscreen.child = hostContainer;
798 + return hostContainer;
799 }
800
801 // Note: These happen to have identical begin phases, for now. We shouldn't hold
@@ -2112,11 +2178,10 @@ function mountSuspensePrimaryChildren(
2178 mode: 'visible',
2179 children: primaryChildren,
2180 };
2115 - const primaryChildFragment = createFiberFromOffscreen(
2181 + const primaryChildFragment = mountWorkInProgressOffscreenFiber(
2182 primaryChildProps,
2183 mode,
2184 renderLanes,
2119 - null,
2185 );
2186 primaryChildFragment.return = workInProgress;
2187 workInProgress.child = primaryChildFragment;
@@ -2149,6 +2214,21 @@ function mountSuspenseFallbackChildren(
2214 primaryChildFragment.childLanes = NoLanes;
2215 primaryChildFragment.pendingProps = primaryChildProps;
2216
2217 + if (
2218 + supportsPersistence &&
2219 + (workInProgress.mode & ConcurrentMode) === NoMode
2220 + ) {
2221 + const isHidden = true;
2222 + const offscreenContainer: Fiber = (primaryChildFragment.child: any);
2223 + const containerProps = {
2224 + hidden: isHidden,
2225 + primaryChildren,
2226 + };
2227 + offscreenContainer.pendingProps = containerProps;
2228 + offscreenContainer.memoizedProps = containerProps;
2229 + completeSuspendedOffscreenHostContainer(null, offscreenContainer);
2230 + }
2231 +
2232 if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2233 // Reset the durations from the first pass so they aren't included in the
2234 // final amounts. This seems counterintuitive, since we're intentionally
@@ -2167,11 +2247,10 @@ function mountSuspenseFallbackChildren(
2247 null,
2248 );
2249 } else {
2170 - primaryChildFragment = createFiberFromOffscreen(
2250 + primaryChildFragment = mountWorkInProgressOffscreenFiber(
2251 primaryChildProps,
2252 mode,
2253 NoLanes,
2174 - null,
2254 );
2255 fallbackChildFragment = createFiberFromFragment(
2256 fallbackChildren,
@@ -2188,7 +2267,17 @@ function mountSuspenseFallbackChildren(
2267 return fallbackChildFragment;
2268 }
2269
2191 -function createWorkInProgressOffscreenFiber(
2270 +function mountWorkInProgressOffscreenFiber(
2271 + offscreenProps: OffscreenProps,
2272 + mode: TypeOfMode,
2273 + renderLanes: Lanes,
2274 +) {
2275 + // The props argument to `createFiberFromOffscreen` is `any` typed, so we use
2276 + // this wrapper function to constrain it.
2277 + return createFiberFromOffscreen(offscreenProps, mode, NoLanes, null);
2278 +}
2279 +
2280 +function updateWorkInProgressOffscreenFiber(
2281 current: Fiber,
2282 offscreenProps: OffscreenProps,
2283 ) {
@@ -2207,7 +2296,7 @@ function updateSuspensePrimaryChildren(
2296 const currentFallbackChildFragment: Fiber | null =
2297 currentPrimaryChildFragment.sibling;
2298
2210 - const primaryChildFragment = createWorkInProgressOffscreenFiber(
2299 + const primaryChildFragment = updateWorkInProgressOffscreenFiber(
2300 currentPrimaryChildFragment,
2301 {
2302 mode: 'visible',
@@ -2282,16 +2371,57 @@ function updateSuspenseFallbackChildren(
2371 currentPrimaryChildFragment.treeBaseDuration;
2372 }
2373
2374 + if (supportsPersistence) {
2375 + // In persistent mode, the offscreen children are wrapped in a host node.
2376 + // We need to complete it now, because we're going to skip over its normal
2377 + // complete phase and go straight to rendering the fallback.
2378 + const isHidden = true;
2379 + const currentOffscreenContainer = currentPrimaryChildFragment.child;
2380 + const offscreenContainer: Fiber = (primaryChildFragment.child: any);
2381 + const containerProps = {
2382 + hidden: isHidden,
2383 + primaryChildren,
2384 + };
2385 + offscreenContainer.pendingProps = containerProps;
2386 + offscreenContainer.memoizedProps = containerProps;
2387 + completeSuspendedOffscreenHostContainer(
2388 + currentOffscreenContainer,
2389 + offscreenContainer,
2390 + );
2391 + }
2392 +
2393 // The fallback fiber was added as a deletion during the first pass.
2394 // However, since we're going to remain on the fallback, we no longer want
2395 // to delete it.
2396 workInProgress.deletions = null;
2397 } else {
2290 - primaryChildFragment = createWorkInProgressOffscreenFiber(
2398 + primaryChildFragment = updateWorkInProgressOffscreenFiber(
2399 currentPrimaryChildFragment,
2400 primaryChildProps,
2401 );
2402
2403 + if (supportsPersistence) {
2404 + // In persistent mode, the offscreen children are wrapped in a host node.
2405 + // We need to complete it now, because we're going to skip over its normal
2406 + // complete phase and go straight to rendering the fallback.
2407 + const currentOffscreenContainer = currentPrimaryChildFragment.child;
2408 + if (currentOffscreenContainer !== null) {
2409 + const isHidden = true;
2410 + const offscreenContainer = reconcileOffscreenHostContainer(
2411 + currentPrimaryChildFragment,
2412 + primaryChildFragment,
2413 + isHidden,
2414 + primaryChildren,
2415 + renderLanes,
2416 + );
2417 + offscreenContainer.memoizedProps = offscreenContainer.pendingProps;
2418 + completeSuspendedOffscreenHostContainer(
2419 + currentOffscreenContainer,
2420 + offscreenContainer,
2421 + );
2422 + }
2423 + }
2424 +
2425 // Since we're reusing a current tree, we need to reuse the flags, too.
2426 // (We don't do this in legacy mode, because in legacy mode we don't re-use
2427 // the current tree; see previous branch.)
@@ -2355,16 +2485,19 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating(
2485 fallbackChildren,
2486 renderLanes,
2487 ) {
2358 - const mode = workInProgress.mode;
2359 - const primaryChildFragment = createFiberFromOffscreen(
2360 - primaryChildren,
2361 - mode,
2488 + const fiberMode = workInProgress.mode;
2489 + const primaryChildProps: OffscreenProps = {
2490 + mode: 'visible',
2491 + children: primaryChildren,
2492 + };
2493 + const primaryChildFragment = mountWorkInProgressOffscreenFiber(
2494 + primaryChildProps,
2495 + fiberMode,
2496 NoLanes,
2363 - null,
2497 );
2498 const fallbackChildFragment = createFiberFromFragment(
2499 fallbackChildren,
2367 - mode,
2500 + fiberMode,
2501 renderLanes,
2502 null,
2503 );
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+68 -67
@@ -84,8 +84,6 @@ import {
84 supportsMutation,
85 supportsPersistence,
86 cloneInstance,
87 - cloneHiddenInstance,
88 - cloneHiddenTextInstance,
87 createContainerChildSet,
88 appendChildToContainerChildSet,
89 finalizeContainerChildren,
@@ -200,12 +198,7 @@ let updateHostText;
198 if (supportsMutation) {
199 // Mutation mode
200
203 - appendAllChildren = function(
204 - parent: Instance,
205 - workInProgress: Fiber,
206 - needsVisibilityToggle: boolean,
207 - isHidden: boolean,
208 - ) {
201 + appendAllChildren = function(parent: Instance, workInProgress: Fiber) {
202 // We only have the top Fiber that was created but we need recurse down its
203 // children to find all the terminal nodes.
204 let node = workInProgress.child;
@@ -293,49 +286,22 @@ if (supportsMutation) {
286 } else if (supportsPersistence) {
287 // Persistent host tree mode
288
296 - appendAllChildren = function(
297 - parent: Instance,
298 - workInProgress: Fiber,
299 - needsVisibilityToggle: boolean,
300 - isHidden: boolean,
301 - ) {
289 + appendAllChildren = function(parent: Instance, workInProgress: Fiber) {
290 // We only have the top Fiber that was created but we need recurse down its
291 // children to find all the terminal nodes.
292 let node = workInProgress.child;
293 while (node !== null) {
294 // eslint-disable-next-line no-labels
295 branches: if (node.tag === HostComponent) {
308 - let instance = node.stateNode;
309 - if (needsVisibilityToggle && isHidden) {
310 - // This child is inside a timed out tree. Hide it.
311 - const props = node.memoizedProps;
312 - const type = node.type;
313 - instance = cloneHiddenInstance(instance, type, props, node);
314 - }
296 + const instance = node.stateNode;
297 appendInitialChild(parent, instance);
298 } else if (node.tag === HostText) {
317 - let instance = node.stateNode;
318 - if (needsVisibilityToggle && isHidden) {
319 - // This child is inside a timed out tree. Hide it.
320 - const text = node.memoizedProps;
321 - instance = cloneHiddenTextInstance(instance, text, node);
322 - }
299 + const instance = node.stateNode;
300 appendInitialChild(parent, instance);
301 } else if (node.tag === HostPortal) {
302 // If we have a portal child, then we don't want to traverse
303 // down its children. Instead, we'll get insertions from each child in
304 // the portal directly.
328 - } else if (
329 - node.tag === OffscreenComponent &&
330 - node.memoizedState !== null
331 - ) {
332 - // The children in this boundary are hidden. Toggle their visibility
333 - // before appending.
334 - const child = node.child;
335 - if (child !== null) {
336 - child.return = node;
337 - }
338 - appendAllChildren(parent, node, true, true);
305 } else if (node.child !== null) {
306 node.child.return = node;
307 node = node.child;
@@ -361,8 +327,6 @@ if (supportsMutation) {
327 const appendAllChildrenToContainer = function(
328 containerChildSet: ChildSet,
329 workInProgress: Fiber,
364 - needsVisibilityToggle: boolean,
365 - isHidden: boolean,
330 ) {
331 // We only have the top Fiber that was created but we need recurse down its
332 // children to find all the terminal nodes.
@@ -370,37 +334,15 @@ if (supportsMutation) {
334 while (node !== null) {
335 // eslint-disable-next-line no-labels
336 branches: if (node.tag === HostComponent) {
373 - let instance = node.stateNode;
374 - if (needsVisibilityToggle && isHidden) {
375 - // This child is inside a timed out tree. Hide it.
376 - const props = node.memoizedProps;
377 - const type = node.type;
378 - instance = cloneHiddenInstance(instance, type, props, node);
379 - }
337 + const instance = node.stateNode;
338 appendChildToContainerChildSet(containerChildSet, instance);
339 } else if (node.tag === HostText) {
382 - let instance = node.stateNode;
383 - if (needsVisibilityToggle && isHidden) {
384 - // This child is inside a timed out tree. Hide it.
385 - const text = node.memoizedProps;
386 - instance = cloneHiddenTextInstance(instance, text, node);
387 - }
340 + const instance = node.stateNode;
341 appendChildToContainerChildSet(containerChildSet, instance);
342 } else if (node.tag === HostPortal) {
343 // If we have a portal child, then we don't want to traverse
344 // down its children. Instead, we'll get insertions from each child in
345 // the portal directly.
393 - } else if (
394 - node.tag === OffscreenComponent &&
395 - node.memoizedState !== null
396 - ) {
397 - // The children in this boundary are hidden. Toggle their visibility
398 - // before appending.
399 - const child = node.child;
400 - if (child !== null) {
401 - child.return = node;
402 - }
403 - appendAllChildrenToContainer(containerChildSet, node, true, true);
346 } else if (node.child !== null) {
347 node.child.return = node;
348 node = node.child;
@@ -434,7 +376,7 @@ if (supportsMutation) {
376 const container = portalOrRoot.containerInfo;
377 const newChildSet = createContainerChildSet(container);
378 // If children might have changed, we have to add them all to the set.
437 - appendAllChildrenToContainer(newChildSet, workInProgress, false, false);
379 + appendAllChildrenToContainer(newChildSet, workInProgress);
380 portalOrRoot.pendingChildren = newChildSet;
381 // Schedule an update on the container to swap out the container.
382 markUpdate(workInProgress);
@@ -507,7 +449,7 @@ if (supportsMutation) {
449 markUpdate(workInProgress);
450 } else {
451 // If children might have changed, we have to add them all to the set.
510 - appendAllChildren(newInstance, workInProgress, false, false);
452 + appendAllChildren(newInstance, workInProgress);
453 }
454 };
455 updateHostText = function(
@@ -748,6 +690,65 @@ function bubbleProperties(completedWork: Fiber) {
690 return didBailout;
691 }
692
693 +export function completeSuspendedOffscreenHostContainer(
694 + current: Fiber | null,
695 + workInProgress: Fiber,
696 +) {
697 + // This is a fork of the complete phase for HostComponent. We use it when
698 + // a suspense tree is in its fallback state, because in that case the primary
699 + // tree that includes the offscreen boundary is skipped over without a
700 + // regular complete phase.
701 + //
702 + // We can optimize this path further by inlining the update logic for
703 + // offscreen instances specifically, i.e. skipping the `prepareUpdate` call.
704 + const rootContainerInstance = getRootHostContainer();
705 + const type = workInProgress.type;
706 + const newProps = workInProgress.memoizedProps;
707 + if (current !== null) {
708 + updateHostComponent(
709 + current,
710 + workInProgress,
711 + type,
712 + newProps,
713 + rootContainerInstance,
714 + );
715 + } else {
716 + const currentHostContext = getHostContext();
717 + const instance = createInstance(
718 + type,
719 + newProps,
720 + rootContainerInstance,
721 + currentHostContext,
722 + workInProgress,
723 + );
724 +
725 + appendAllChildren(instance, workInProgress);
726 +
727 + workInProgress.stateNode = instance;
728 +
729 + // Certain renderers require commit-time effects for initial mount.
730 + // (eg DOM renderer supports auto-focus for certain elements).
731 + // Make sure such renderers get scheduled for later work.
732 + if (
733 + finalizeInitialChildren(
734 + instance,
735 + type,
736 + newProps,
737 + rootContainerInstance,
738 + currentHostContext,
739 + )
740 + ) {
741 + markUpdate(workInProgress);
742 + }
743 +
744 + if (workInProgress.ref !== null) {
745 + // If there is a ref on a host node we need to schedule a callback
746 + markRef(workInProgress);
747 + }
748 + }
749 + bubbleProperties(workInProgress);
750 +}
751 +
752 function completeWork(
753 current: Fiber | null,
754 workInProgress: Fiber,
@@ -868,7 +869,7 @@ function completeWork(
869 workInProgress,
870 );
871
871 - appendAllChildren(instance, workInProgress, false, false);
872 + appendAllChildren(instance, workInProgress);
873
874 workInProgress.stateNode = instance;
875
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+68 -67
@@ -84,8 +84,6 @@ import {
84 supportsMutation,
85 supportsPersistence,
86 cloneInstance,
87 - cloneHiddenInstance,
88 - cloneHiddenTextInstance,
87 createContainerChildSet,
88 appendChildToContainerChildSet,
89 finalizeContainerChildren,
@@ -200,12 +198,7 @@ let updateHostText;
198 if (supportsMutation) {
199 // Mutation mode
200
203 - appendAllChildren = function(
204 - parent: Instance,
205 - workInProgress: Fiber,
206 - needsVisibilityToggle: boolean,
207 - isHidden: boolean,
208 - ) {
201 + appendAllChildren = function(parent: Instance, workInProgress: Fiber) {
202 // We only have the top Fiber that was created but we need recurse down its
203 // children to find all the terminal nodes.
204 let node = workInProgress.child;
@@ -293,49 +286,22 @@ if (supportsMutation) {
286 } else if (supportsPersistence) {
287 // Persistent host tree mode
288
296 - appendAllChildren = function(
297 - parent: Instance,
298 - workInProgress: Fiber,
299 - needsVisibilityToggle: boolean,
300 - isHidden: boolean,
301 - ) {
289 + appendAllChildren = function(parent: Instance, workInProgress: Fiber) {
290 // We only have the top Fiber that was created but we need recurse down its
291 // children to find all the terminal nodes.
292 let node = workInProgress.child;
293 while (node !== null) {
294 // eslint-disable-next-line no-labels
295 branches: if (node.tag === HostComponent) {
308 - let instance = node.stateNode;
309 - if (needsVisibilityToggle && isHidden) {
310 - // This child is inside a timed out tree. Hide it.
311 - const props = node.memoizedProps;
312 - const type = node.type;
313 - instance = cloneHiddenInstance(instance, type, props, node);
314 - }
296 + const instance = node.stateNode;
297 appendInitialChild(parent, instance);
298 } else if (node.tag === HostText) {
317 - let instance = node.stateNode;
318 - if (needsVisibilityToggle && isHidden) {
319 - // This child is inside a timed out tree. Hide it.
320 - const text = node.memoizedProps;
321 - instance = cloneHiddenTextInstance(instance, text, node);
322 - }
299 + const instance = node.stateNode;
300 appendInitialChild(parent, instance);
301 } else if (node.tag === HostPortal) {
302 // If we have a portal child, then we don't want to traverse
303 // down its children. Instead, we'll get insertions from each child in
304 // the portal directly.
328 - } else if (
329 - node.tag === OffscreenComponent &&
330 - node.memoizedState !== null
331 - ) {
332 - // The children in this boundary are hidden. Toggle their visibility
333 - // before appending.
334 - const child = node.child;
335 - if (child !== null) {
336 - child.return = node;
337 - }
338 - appendAllChildren(parent, node, true, true);
305 } else if (node.child !== null) {
306 node.child.return = node;
307 node = node.child;
@@ -361,8 +327,6 @@ if (supportsMutation) {
327 const appendAllChildrenToContainer = function(
328 containerChildSet: ChildSet,
329 workInProgress: Fiber,
364 - needsVisibilityToggle: boolean,
365 - isHidden: boolean,
330 ) {
331 // We only have the top Fiber that was created but we need recurse down its
332 // children to find all the terminal nodes.
@@ -370,37 +334,15 @@ if (supportsMutation) {
334 while (node !== null) {
335 // eslint-disable-next-line no-labels
336 branches: if (node.tag === HostComponent) {
373 - let instance = node.stateNode;
374 - if (needsVisibilityToggle && isHidden) {
375 - // This child is inside a timed out tree. Hide it.
376 - const props = node.memoizedProps;
377 - const type = node.type;
378 - instance = cloneHiddenInstance(instance, type, props, node);
379 - }
337 + const instance = node.stateNode;
338 appendChildToContainerChildSet(containerChildSet, instance);
339 } else if (node.tag === HostText) {
382 - let instance = node.stateNode;
383 - if (needsVisibilityToggle && isHidden) {
384 - // This child is inside a timed out tree. Hide it.
385 - const text = node.memoizedProps;
386 - instance = cloneHiddenTextInstance(instance, text, node);
387 - }
340 + const instance = node.stateNode;
341 appendChildToContainerChildSet(containerChildSet, instance);
342 } else if (node.tag === HostPortal) {
343 // If we have a portal child, then we don't want to traverse
344 // down its children. Instead, we'll get insertions from each child in
345 // the portal directly.
393 - } else if (
394 - node.tag === OffscreenComponent &&
395 - node.memoizedState !== null
396 - ) {
397 - // The children in this boundary are hidden. Toggle their visibility
398 - // before appending.
399 - const child = node.child;
400 - if (child !== null) {
401 - child.return = node;
402 - }
403 - appendAllChildrenToContainer(containerChildSet, node, true, true);
346 } else if (node.child !== null) {
347 node.child.return = node;
348 node = node.child;
@@ -434,7 +376,7 @@ if (supportsMutation) {
376 const container = portalOrRoot.containerInfo;
377 const newChildSet = createContainerChildSet(container);
378 // If children might have changed, we have to add them all to the set.
437 - appendAllChildrenToContainer(newChildSet, workInProgress, false, false);
379 + appendAllChildrenToContainer(newChildSet, workInProgress);
380 portalOrRoot.pendingChildren = newChildSet;
381 // Schedule an update on the container to swap out the container.
382 markUpdate(workInProgress);
@@ -507,7 +449,7 @@ if (supportsMutation) {
449 markUpdate(workInProgress);
450 } else {
451 // If children might have changed, we have to add them all to the set.
510 - appendAllChildren(newInstance, workInProgress, false, false);
452 + appendAllChildren(newInstance, workInProgress);
453 }
454 };
455 updateHostText = function(
@@ -748,6 +690,65 @@ function bubbleProperties(completedWork: Fiber) {
690 return didBailout;
691 }
692
693 +export function completeSuspendedOffscreenHostContainer(
694 + current: Fiber | null,
695 + workInProgress: Fiber,
696 +) {
697 + // This is a fork of the complete phase for HostComponent. We use it when
698 + // a suspense tree is in its fallback state, because in that case the primary
699 + // tree that includes the offscreen boundary is skipped over without a
700 + // regular complete phase.
701 + //
702 + // We can optimize this path further by inlining the update logic for
703 + // offscreen instances specifically, i.e. skipping the `prepareUpdate` call.
704 + const rootContainerInstance = getRootHostContainer();
705 + const type = workInProgress.type;
706 + const newProps = workInProgress.memoizedProps;
707 + if (current !== null) {
708 + updateHostComponent(
709 + current,
710 + workInProgress,
711 + type,
712 + newProps,
713 + rootContainerInstance,
714 + );
715 + } else {
716 + const currentHostContext = getHostContext();
717 + const instance = createInstance(
718 + type,
719 + newProps,
720 + rootContainerInstance,
721 + currentHostContext,
722 + workInProgress,
723 + );
724 +
725 + appendAllChildren(instance, workInProgress);
726 +
727 + workInProgress.stateNode = instance;
728 +
729 + // Certain renderers require commit-time effects for initial mount.
730 + // (eg DOM renderer supports auto-focus for certain elements).
731 + // Make sure such renderers get scheduled for later work.
732 + if (
733 + finalizeInitialChildren(
734 + instance,
735 + type,
736 + newProps,
737 + rootContainerInstance,
738 + currentHostContext,
739 + )
740 + ) {
741 + markUpdate(workInProgress);
742 + }
743 +
744 + if (workInProgress.ref !== null) {
745 + // If there is a ref on a host node we need to schedule a callback
746 + markRef(workInProgress);
747 + }
748 + }
749 + bubbleProperties(workInProgress);
750 +}
751 +
752 function completeWork(
753 current: Fiber | null,
754 workInProgress: Fiber,
@@ -868,7 +869,7 @@ function completeWork(
869 workInProgress,
870 );
871
871 - appendAllChildren(instance, workInProgress, false, false);
872 + appendAllChildren(instance, workInProgress);
873
874 workInProgress.stateNode = instance;
875
packages/react-reconciler/src/ReactFiberHostConfigWithNoPersistence.js
+2 -2
@@ -28,5 +28,5 @@ export const createContainerChildSet = shim;
28 export const appendChildToContainerChildSet = shim;
29 export const finalizeContainerChildren = shim;
30 export const replaceContainerChildren = shim;
31 -export const cloneHiddenInstance = shim;
32 -export const cloneHiddenTextInstance = shim;
31 +export const getOffscreenContainerType = shim;
32 +export const getOffscreenContainerProps = shim;
packages/react-reconciler/src/ReactFiberOffscreenComponent.js
+2 -2
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {ReactNodeList} from 'shared/ReactTypes';
10 +import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes';
11 import type {Lanes} from './ReactFiberLane.old';
12 import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
13
@@ -18,7 +18,7 @@ export type OffscreenProps = {|
18 //
19 // Default mode is visible. Kind of a weird default for a component
20 // called "Offscreen." Possible alt: <Visibility />?
21 - mode?: 'hidden' | 'unstable-defer-without-hiding' | 'visible' | null | void,
21 + mode?: OffscreenMode | null | void,
22 children?: ReactNodeList,
23 |};
24
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+4 -2
@@ -127,8 +127,10 @@ export const appendChildToContainerChildSet =
127 export const finalizeContainerChildren =
128 $$$hostConfig.finalizeContainerChildren;
129 export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren;
130 -export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance;
131 -export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
130 +export const getOffscreenContainerType =
131 + $$$hostConfig.getOffscreenContainerType;
132 +export const getOffscreenContainerProps =
133 + $$$hostConfig.getOffscreenContainerProps;
134
135 // -------------------
136 // Hydration
packages/shared/ReactTypes.js
+5
@@ -166,3 +166,8 @@ export interface Thenable<+R> {
166 onReject: (error: mixed) => void | Thenable<U> | U,
167 ): void | Thenable<U>;
168 }
169 +
170 +export type OffscreenMode =
171 + | 'hidden'
172 + | 'unstable-defer-without-hiding'
173 + | 'visible';