@samitouri / QOS-React-2 / commits / cb70753993

Move hide/unhide logic to Offscreen component

The Offscreen component is not a public type, yet, but once it is, it will share the same hide/unhide logic as Suspense children.

Andrew Clark committed Apr 23, 2020 at 16:38 UTC cb7075399376f4b913500c4e377d790138b31c74
5 files changed +159 -31
packages/react-reconciler/src/ReactFiber.new.js
+2 -1
@@ -20,6 +20,7 @@ import type {WorkTag} from './ReactWorkTags';
20 import type {TypeOfMode} from './ReactTypeOfMode';
21 import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
22 import type {SuspenseInstance} from './ReactFiberHostConfig';
23 +import type {OffscreenProps} from './ReactFiberOffscreenComponent';
24
25 import invariant from 'shared/invariant';
26 import {
@@ -738,7 +739,7 @@ export function createFiberFromSuspenseList(
739 }
740
741 export function createFiberFromOffscreen(
741 - pendingProps: any,
742 + pendingProps: OffscreenProps,
743 mode: TypeOfMode,
744 expirationTime: ExpirationTimeOpaque,
745 key: null | string,
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+81 -10
@@ -19,6 +19,10 @@ import type {
19 SuspenseListTailMode,
20 } from './ReactFiberSuspenseComponent.new';
21 import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
22 +import type {
23 + OffscreenProps,
24 + OffscreenState,
25 +} from './ReactFiberOffscreenComponent';
26
27 import checkPropTypes from 'shared/checkPropTypes';
28
@@ -562,7 +566,20 @@ function updateOffscreenComponent(
566 workInProgress: Fiber,
567 renderExpirationTime: ExpirationTimeOpaque,
568 ) {
565 - const nextChildren = workInProgress.pendingProps;
569 + const nextProps: OffscreenProps = workInProgress.pendingProps;
570 + const nextChildren = nextProps.children;
571 +
572 + if (current !== null) {
573 + if (nextProps.mode === 'hidden') {
574 + // TODO: Should currently be unreachable because Offscreen is only used as
575 + // an implementation detail of Suspense. Once this is a public API, it
576 + // will need to create an OffscreenState.
577 + } else {
578 + // Clear the offscreen state.
579 + workInProgress.memoizedState = null;
580 + }
581 + }
582 +
583 reconcileChildren(
584 current,
585 workInProgress,
@@ -1854,12 +1871,16 @@ function updateSuspenseComponent(
1871 }
1872
1873 if (showFallback) {
1874 + const nextPrimaryChildren = nextProps.children;
1875 const nextFallbackChildren = nextProps.fallback;
1876 const fallbackFragment = mountSuspenseFallbackChildren(
1877 workInProgress,
1878 + nextPrimaryChildren,
1879 nextFallbackChildren,
1880 renderExpirationTime,
1881 );
1882 + const primaryChildFragment: Fiber = (workInProgress.child: any);
1883 + primaryChildFragment.memoizedState = ({baseTime: NoWork}: OffscreenState);
1884 workInProgress.memoizedState = mountSuspenseState(renderExpirationTime);
1885 return fallbackFragment;
1886 } else {
@@ -1904,14 +1925,19 @@ function updateSuspenseComponent(
1925 } else {
1926 // Suspended but we should no longer be in dehydrated mode.
1927 // Therefore we now have to render the fallback.
1928 + const nextPrimaryChildren = nextProps.children;
1929 const nextFallbackChildren = nextProps.fallback;
1930 const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
1931 current,
1932 workInProgress,
1933 + nextPrimaryChildren,
1934 nextFallbackChildren,
1935 renderExpirationTime,
1936 );
1914 -
1937 + const primaryChildFragment: Fiber = (workInProgress.child: any);
1938 + primaryChildFragment.memoizedState = ({
1939 + baseTime: NoWork,
1940 + }: OffscreenState);
1941 workInProgress.memoizedState = updateSuspenseState(
1942 current.memoizedState,
1943 renderExpirationTime,
@@ -1924,13 +1950,18 @@ function updateSuspenseComponent(
1950
1951 if (showFallback) {
1952 const nextFallbackChildren = nextProps.fallback;
1953 + const nextPrimaryChildren = nextProps.children;
1954 const fallbackChildFragment = updateSuspenseFallbackChildren(
1955 current,
1956 workInProgress,
1957 + nextPrimaryChildren,
1958 nextFallbackChildren,
1959 renderExpirationTime,
1960 );
1961 const primaryChildFragment: Fiber = (workInProgress.child: any);
1962 + primaryChildFragment.memoizedState = ({
1963 + baseTime: NoWork,
1964 + }: OffscreenState);
1965 primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
1966 current,
1967 workInProgress,
@@ -1957,13 +1988,18 @@ function updateSuspenseComponent(
1988 if (showFallback) {
1989 // Timed out.
1990 const nextFallbackChildren = nextProps.fallback;
1991 + const nextPrimaryChildren = nextProps.children;
1992 const fallbackChildFragment = updateSuspenseFallbackChildren(
1993 current,
1994 workInProgress,
1995 + nextPrimaryChildren,
1996 nextFallbackChildren,
1997 renderExpirationTime,
1998 );
1999 const primaryChildFragment: Fiber = (workInProgress.child: any);
2000 + primaryChildFragment.memoizedState = ({
2001 + baseTime: NoWork,
2002 + }: OffscreenState);
2003 primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
2004 current,
2005 workInProgress,
@@ -1996,8 +2032,12 @@ function mountSuspensePrimaryChildren(
2032 renderExpirationTime,
2033 ) {
2034 const mode = workInProgress.mode;
2035 + const primaryChildProps: OffscreenProps = {
2036 + mode: 'visible',
2037 + children: primaryChildren,
2038 + };
2039 const primaryChildFragment = createFiberFromOffscreen(
2000 - primaryChildren,
2040 + primaryChildProps,
2041 mode,
2042 renderExpirationTime,
2043 null,
@@ -2009,13 +2049,18 @@ function mountSuspensePrimaryChildren(
2049
2050 function mountSuspenseFallbackChildren(
2051 workInProgress,
2052 + primaryChildren,
2053 fallbackChildren,
2054 renderExpirationTime,
2055 ) {
2056 const mode = workInProgress.mode;
2016 -
2057 const progressedPrimaryFragment: Fiber | null = workInProgress.child;
2058
2059 + const primaryChildProps: OffscreenProps = {
2060 + mode: 'hidden',
2061 + children: primaryChildren,
2062 + };
2063 +
2064 let primaryChildFragment;
2065 let fallbackChildFragment;
2066 if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
@@ -2023,6 +2068,7 @@ function mountSuspenseFallbackChildren(
2068 // completed, even though it's in an inconsistent state.
2069 primaryChildFragment = progressedPrimaryFragment;
2070 primaryChildFragment.childExpirationTime_opaque = NoWork;
2071 + primaryChildFragment.pendingProps = primaryChildProps;
2072
2073 if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2074 // Reset the durations from the first pass so they aren't included in the
@@ -2042,7 +2088,12 @@ function mountSuspenseFallbackChildren(
2088 null,
2089 );
2090 } else {
2045 - primaryChildFragment = createFiberFromOffscreen(null, mode, NoWork, null);
2091 + primaryChildFragment = createFiberFromOffscreen(
2092 + primaryChildProps,
2093 + mode,
2094 + NoWork,
2095 + null,
2096 + );
2097 fallbackChildFragment = createFiberFromFragment(
2098 fallbackChildren,
2099 mode,
@@ -2058,6 +2109,15 @@ function mountSuspenseFallbackChildren(
2109 return fallbackChildFragment;
2110 }
2111
2112 +function createWorkInProgressOffscreenFiber(
2113 + current: Fiber,
2114 + offscreenProps: OffscreenProps,
2115 +) {
2116 + // The props argument to `createWorkInProgress` is `any` typed, so we use this
2117 + // wrapper function to constrain it.
2118 + return createWorkInProgress(current, offscreenProps);
2119 +}
2120 +
2121 function updateSuspensePrimaryChildren(
2122 current,
2123 workInProgress,
@@ -2068,9 +2128,12 @@ function updateSuspensePrimaryChildren(
2128 const currentFallbackChildFragment: Fiber | null =
2129 currentPrimaryChildFragment.sibling;
2130
2071 - const primaryChildFragment = createWorkInProgress(
2131 + const primaryChildFragment = createWorkInProgressOffscreenFiber(
2132 currentPrimaryChildFragment,
2073 - primaryChildren,
2133 + {
2134 + mode: 'visible',
2135 + children: primaryChildren,
2136 + },
2137 );
2138 if ((workInProgress.mode & BlockingMode) === NoMode) {
2139 primaryChildFragment.expirationTime_opaque = renderExpirationTime;
@@ -2091,6 +2154,7 @@ function updateSuspensePrimaryChildren(
2154 function updateSuspenseFallbackChildren(
2155 current,
2156 workInProgress,
2157 + primaryChildren,
2158 fallbackChildren,
2159 renderExpirationTime,
2160 ) {
@@ -2099,6 +2163,11 @@ function updateSuspenseFallbackChildren(
2163 const currentFallbackChildFragment: Fiber | null =
2164 currentPrimaryChildFragment.sibling;
2165
2166 + const primaryChildProps: OffscreenProps = {
2167 + mode: 'hidden',
2168 + children: primaryChildren,
2169 + };
2170 +
2171 let primaryChildFragment;
2172 if ((mode & BlockingMode) === NoMode) {
2173 // In legacy mode, we commit the primary tree as if it successfully
@@ -2106,6 +2175,7 @@ function updateSuspenseFallbackChildren(
2175 const progressedPrimaryFragment: Fiber = (workInProgress.child: any);
2176 primaryChildFragment = progressedPrimaryFragment;
2177 primaryChildFragment.childExpirationTime_opaque = NoWork;
2178 + primaryChildFragment.pendingProps = primaryChildProps;
2179
2180 if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2181 // Reset the durations from the first pass so they aren't included in the
@@ -2136,9 +2206,9 @@ function updateSuspenseFallbackChildren(
2206 workInProgress.firstEffect = workInProgress.lastEffect = null;
2207 }
2208 } else {
2139 - primaryChildFragment = createWorkInProgress(
2209 + primaryChildFragment = createWorkInProgressOffscreenFiber(
2210 currentPrimaryChildFragment,
2141 - currentPrimaryChildFragment.pendingProps,
2211 + primaryChildProps,
2212 );
2213 }
2214 let fallbackChildFragment;
@@ -2199,12 +2269,13 @@ function retrySuspenseComponentWithoutHydrating(
2269 function mountSuspenseFallbackAfterRetryWithoutHydrating(
2270 current,
2271 workInProgress,
2272 + primaryChildren,
2273 fallbackChildren,
2274 renderExpirationTime,
2275 ) {
2276 const mode = workInProgress.mode;
2277 const primaryChildFragment = createFiberFromOffscreen(
2207 - null,
2278 + primaryChildren,
2279 mode,
2280 NoWork,
2281 null,
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+31 -19
@@ -23,6 +23,7 @@ import type {UpdateQueue} from './ReactUpdateQueue.new';
23 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
24 import type {Wakeable} from 'shared/ReactTypes';
25 import type {ReactPriorityLevel} from './ReactInternalTypes';
26 +import type {OffscreenState} from './ReactFiberOffscreenComponent';
27
28 import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
29 import {
@@ -55,6 +56,7 @@ import {
56 FundamentalComponent,
57 ScopeComponent,
58 Block,
59 + OffscreenComponent,
60 } from './ReactWorkTags';
61 import {
62 invokeGuardedCallback,
@@ -805,6 +807,7 @@ function commitLifeCycles(
807 case IncompleteClassComponent:
808 case FundamentalComponent:
809 case ScopeComponent:
810 + case OffscreenComponent:
811 return;
812 }
813 invariant(
@@ -835,16 +838,12 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
838 unhideTextInstance(instance, node.memoizedProps);
839 }
840 } else if (
838 - node.tag === SuspenseComponent &&
839 - node.memoizedState !== null &&
840 - node.memoizedState.dehydrated === null
841 + node.tag === OffscreenComponent &&
842 + (node.memoizedState: OffscreenState) !== null &&
843 + node !== finishedWork
844 ) {
842 - // Found a nested Suspense component that timed out. Skip over the
843 - // primary child fragment, which should remain hidden.
844 - const fallbackChildFragment: Fiber = (node.child: any).sibling;
845 - fallbackChildFragment.return = node;
846 - node = fallbackChildFragment;
847 - continue;
845 + // Found a nested Offscreen component that is hidden. Don't search
846 + // any deeper. This tree should remain hidden.
847 } else if (node.child !== null) {
848 node.child.return = node;
849 node = node.child;
@@ -1584,6 +1583,9 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1583 }
1584 break;
1585 }
1586 + case OffscreenComponent: {
1587 + return;
1588 + }
1589 }
1590
1591 commitContainer(finishedWork);
@@ -1720,6 +1722,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1722 }
1723 break;
1724 }
1725 + case OffscreenComponent: {
1726 + const newState: OffscreenState | null = finishedWork.memoizedState;
1727 + const isHidden = newState !== null;
1728 + hideOrUnhideAllChildren(finishedWork, isHidden);
1729 + return;
1730 + }
1731 }
1732 invariant(
1733 false,
@@ -1731,18 +1739,22 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1739 function commitSuspenseComponent(finishedWork: Fiber) {
1740 const newState: SuspenseState | null = finishedWork.memoizedState;
1741
1734 - let newDidTimeout;
1735 - let primaryChildParent = finishedWork;
1736 - if (newState === null) {
1737 - newDidTimeout = false;
1738 - } else {
1739 - newDidTimeout = true;
1740 - primaryChildParent = finishedWork.child;
1742 + if (newState !== null) {
1743 markCommitTimeOfFallback();
1742 - }
1744
1744 - if (supportsMutation && primaryChildParent !== null) {
1745 - hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
1745 + if (supportsMutation) {
1746 + // Hide the Offscreen component that contains the primary children. TODO:
1747 + // Ideally, this effect would have been scheduled on the Offscreen fiber
1748 + // itself. That's how unhiding works: the Offscreen component schedules an
1749 + // effect on itself. However, in this case, the component didn't complete,
1750 + // so the fiber was never added to the effect list in the normal path. We
1751 + // could have appended it to the effect list in the Suspense component's
1752 + // second pass, but doing it this way is less complicated. This would be
1753 + // simpler if we got rid of the effect list and traversed the tree, like
1754 + // we're planning to do.
1755 + const primaryChildParent: Fiber = (finishedWork.child: any);
1756 + hideOrUnhideAllChildren(primaryChildParent, true);
1757 + }
1758 }
1759
1760 if (enableSuspenseCallback && newState !== null) {
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+14 -1
@@ -26,6 +26,8 @@ import type {
26 SuspenseListRenderState,
27 } from './ReactFiberSuspenseComponent.new';
28 import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
29 +import type {OffscreenState} from './ReactFiberOffscreenComponent';
30 +
31 import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
32
33 import {now} from './SchedulerWithReactIntegration.new';
@@ -1288,8 +1290,19 @@ function completeWork(
1290 return null;
1291 }
1292 break;
1291 - case OffscreenComponent:
1293 + case OffscreenComponent: {
1294 + if (current !== null) {
1295 + const nextState: OffscreenState | null = workInProgress.memoizedState;
1296 + const prevState: OffscreenState | null = current.memoizedState;
1297 +
1298 + const prevIsHidden = prevState !== null;
1299 + const nextIsHidden = nextState !== null;
1300 + if (prevIsHidden !== nextIsHidden) {
1301 + workInProgress.effectTag |= Update;
1302 + }
1303 + }
1304 return null;
1305 + }
1306 }
1307 invariant(
1308 false,
packages/react-reconciler/src/ReactFiberOffscreenComponent.js new
+31
@@ -0,0 +1,31 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactNodeList} from 'shared/ReactTypes';
11 +import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
12 +
13 +export type OffscreenProps = {|
14 + // TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
15 + // for now, since we might have multiple variants. For example, hiding the
16 + // content without changing the layout.
17 + //
18 + // Default mode is visible. Kind of a weird default for a component
19 + // called "Offscreen." Possible alt: <Visibility />?
20 + mode?: 'hidden' | 'visible' | null | void,
21 + children?: ReactNodeList,
22 +|};
23 +
24 +// We use the existence of the state object as an indicator that the component
25 +// is hidden.
26 +export type OffscreenState = {|
27 + // TODO: This doesn't do anything, yet. It's always NoWork. But eventually it
28 + // will represent the pending work that must be included in the render in
29 + // order to unhide the component.
30 + baseTime: ExpirationTimeOpaque,
31 +|};