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
+|};