13
import type {ExpirationTime} from './ReactFiberExpirationTime';
14
import type {
15
SuspenseState,
16
- SuspenseListState,
16
+ SuspenseListRenderState,
17
} from './ReactFiberSuspenseComponent';
18
import type {SuspenseContext} from './ReactFiberSuspenseContext';
19
2007
2008
type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void;
2009
2010
+function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
2011
+ if (__DEV__) {
2012
+ if (
2013
+ revealOrder !== undefined &&
2014
+ revealOrder !== 'forwards' &&
2015
+ revealOrder !== 'backwards' &&
2016
+ revealOrder !== 'together' &&
2017
+ !didWarnAboutRevealOrder[revealOrder]
2018
+ ) {
2019
+ didWarnAboutRevealOrder[revealOrder] = true;
2020
+ if (typeof revealOrder === 'string') {
2021
+ switch (revealOrder.toLowerCase()) {
2022
+ case 'together':
2023
+ case 'forwards':
2024
+ case 'backwards': {
2025
+ warning(
2026
+ false,
2027
+ '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2028
+ 'Use lowercase "%s" instead.',
2029
+ revealOrder,
2030
+ revealOrder.toLowerCase(),
2031
+ );
2032
+ break;
2033
+ }
2034
+ case 'forward':
2035
+ case 'backward': {
2036
+ warning(
2037
+ false,
2038
+ '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2039
+ 'React uses the -s suffix in the spelling. Use "%ss" instead.',
2040
+ revealOrder,
2041
+ revealOrder.toLowerCase(),
2042
+ );
2043
+ break;
2044
+ }
2045
+ default:
2046
+ warning(
2047
+ false,
2048
+ '"%s" is not a supported revealOrder on <SuspenseList />. ' +
2049
+ 'Did you mean "together", "forwards" or "backwards"?',
2050
+ revealOrder,
2051
+ );
2052
+ break;
2053
+ }
2054
+ } else {
2055
+ warning(
2056
+ false,
2057
+ '%s is not a supported value for revealOrder on <SuspenseList />. ' +
2058
+ 'Did you mean "together", "forwards" or "backwards"?',
2059
+ revealOrder,
2060
+ );
2061
+ }
2062
+ }
2063
+ }
2064
+}
2065
+
2066
+function initSuspenseListRenderState(
2067
+ workInProgress: Fiber,
2068
+ isBackwards: boolean,
2069
+ tail: null | Fiber,
2070
+ lastContentRow: null | Fiber,
2071
+): void {
2072
+ let renderState: null | SuspenseListRenderState =
2073
+ workInProgress.memoizedState;
2074
+ if (renderState === null) {
2075
+ workInProgress.memoizedState = {
2076
+ isBackwards: isBackwards,
2077
+ rendering: null,
2078
+ last: lastContentRow,
2079
+ tail: tail,
2080
+ tailExpiration: 0,
2081
+ };
2082
+ } else {
2083
+ // We can reuse the existing object from previous renders.
2084
+ renderState.isBackwards = isBackwards;
2085
+ renderState.rendering = null;
2086
+ renderState.last = lastContentRow;
2087
+ renderState.tail = tail;
2088
+ renderState.tailExpiration = 0;
2089
+ }
2090
+}
2091
+
2092
// This can end up rendering this component multiple passes.
2093
// The first pass splits the children fibers into two sets. A head and tail.
2094
// We first render the head. If anything is in fallback state, we do another
2103
) {
2104
const nextProps = workInProgress.pendingProps;
2105
const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
2024
- const nextChildren = nextProps.children;
2106
+ const newChildren = nextProps.children;
2107
2026
- let nextChildFibers;
2027
- if (current === null) {
2028
- nextChildFibers = mountChildFibers(
2029
- workInProgress,
2030
- null,
2031
- nextChildren,
2032
- renderExpirationTime,
2033
- );
2034
- } else {
2035
- nextChildFibers = reconcileChildFibers(
2036
- workInProgress,
2037
- current.child,
2038
- nextChildren,
2039
- renderExpirationTime,
2040
- );
2041
- }
2108
+ validateRevealOrder(revealOrder);
2109
+
2110
+ reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
2111
2112
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
2113
2115
suspenseContext,
2116
(ForceSuspenseFallback: SuspenseContext),
2117
);
2049
-
2050
- if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
2051
- // This is the second pass. In this pass, we should force the
2052
- // fallbacks in place.
2053
- shouldForceFallback = true;
2054
- }
2055
-
2056
- let suspenseListState: null | SuspenseListState = null;
2057
-
2118
if (shouldForceFallback) {
2119
suspenseContext = setShallowSuspenseContext(
2120
suspenseContext,
2121
ForceSuspenseFallback,
2122
);
2063
- suspenseListState = {
2064
- didSuspend: true,
2065
- isBackwards: false,
2066
- rendering: null,
2067
- last: null,
2068
- tail: null,
2069
- tailExpiration: 0,
2070
- };
2123
+ workInProgress.effectTag |= DidCapture;
2124
} else {
2072
- let didForceFallback =
2073
- current !== null &&
2074
- current.memoizedState !== null &&
2075
- (current.memoizedState: SuspenseListState).didSuspend;
2076
- if (didForceFallback) {
2125
+ const didSuspendBefore =
2126
+ current !== null && (current.effectTag & DidCapture) !== NoEffect;
2127
+ if (didSuspendBefore) {
2128
// If we previously forced a fallback, we need to schedule work
2129
// on any nested boundaries to let them know to try to render
2130
// again. This is the same as context updating.
2131
propagateSuspenseContextChange(
2132
workInProgress,
2082
- nextChildFibers,
2133
+ workInProgress.child,
2134
renderExpirationTime,
2135
);
2136
}
2137
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
2138
}
2088
-
2139
pushSuspenseContext(workInProgress, suspenseContext);
2140
2141
if ((workInProgress.mode & BatchedMode) === NoMode) {
2142
+ workInProgress.memoizedState = null;
2143
+ } else {
2144
// Outside of batched mode, SuspenseList doesn't work so we just
2145
// use make it a noop by treating it as the default revealOrder.
2094
- workInProgress.effectTag |= DidCapture;
2095
- workInProgress.child = nextChildFibers;
2096
- return nextChildFibers;
2097
- }
2098
-
2099
- switch (revealOrder) {
2100
- case 'forwards': {
2101
- // If need to force fallbacks in this pass we're just going to
2102
- // force the whole set to suspend so we don't have to do anything
2103
- // further here.
2104
- if (!shouldForceFallback) {
2105
- let lastContentRow = findLastContentRow(nextChildFibers);
2146
+ switch (revealOrder) {
2147
+ case 'forwards': {
2148
+ let lastContentRow = findLastContentRow(workInProgress.child);
2149
let tail;
2150
if (lastContentRow === null) {
2151
// The whole list is part of the tail.
2152
// TODO: We could fast path by just rendering the tail now.
2110
- tail = nextChildFibers;
2111
- nextChildFibers = null;
2153
+ tail = workInProgress.child;
2154
+ workInProgress.child = null;
2155
} else {
2156
// Disconnect the tail rows after the content row.
2157
// We're going to render them separately later.
2158
tail = lastContentRow.sibling;
2159
lastContentRow.sibling = null;
2160
}
2118
- if (suspenseListState === null) {
2119
- suspenseListState = {
2120
- didSuspend: false,
2121
- isBackwards: false,
2122
- rendering: null,
2123
- last: lastContentRow,
2124
- tail: tail,
2125
- tailExpiration: 0,
2126
- };
2127
- } else {
2128
- suspenseListState.tail = tail;
2129
- }
2161
+ initSuspenseListRenderState(
2162
+ workInProgress,
2163
+ false, // isBackwards
2164
+ tail,
2165
+ lastContentRow,
2166
+ );
2167
+ break;
2168
}
2131
- break;
2132
- }
2133
- case 'backwards': {
2134
- // If need to force fallbacks in this pass we're just going to
2135
- // force the whole set to suspend so we don't have to do anything
2136
- // further here.
2137
- if (!shouldForceFallback) {
2169
+ case 'backwards': {
2170
// We're going to find the first row that has existing content.
2171
// At the same time we're going to reverse the list of everything
2172
// we pass in the meantime. That's going to be our tail in reverse
2173
// order.
2174
let tail = null;
2143
- let row = nextChildFibers;
2144
- nextChildFibers = null;
2175
+ let row = workInProgress.child;
2176
+ workInProgress.child = null;
2177
while (row !== null) {
2178
let currentRow = row.alternate;
2179
// New rows can't be content rows.
2180
if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
2181
// This is the beginning of the main content.
2150
- nextChildFibers = row;
2182
+ workInProgress.child = row;
2183
break;
2184
}
2185
let nextRow = row.sibling;
2187
tail = row;
2188
row = nextRow;
2189
}
2158
- // TODO: If nextChildFibers is null, we can continue on the tail immediately.
2159
- if (suspenseListState === null) {
2160
- suspenseListState = {
2161
- didSuspend: false,
2162
- isBackwards: true,
2163
- rendering: null,
2164
- last: null,
2165
- tail: tail,
2166
- tailExpiration: 0,
2167
- };
2168
- } else {
2169
- suspenseListState.isBackwards = true;
2170
- suspenseListState.tail = tail;
2171
- }
2190
+ // TODO: If workInProgress.child is null, we can continue on the tail immediately.
2191
+ initSuspenseListRenderState(
2192
+ workInProgress,
2193
+ true, // isBackwards
2194
+ tail,
2195
+ null, // last
2196
+ );
2197
+ break;
2198
}
2173
- break;
2174
- }
2175
- case 'together': {
2176
- break;
2177
- }
2178
- default: {
2179
- // The default reveal order is the same as not having
2180
- // a boundary.
2181
- if (__DEV__) {
2182
- if (
2183
- revealOrder !== undefined &&
2184
- !didWarnAboutRevealOrder[revealOrder]
2185
- ) {
2186
- didWarnAboutRevealOrder[revealOrder] = true;
2187
- if (typeof revealOrder === 'string') {
2188
- switch (revealOrder.toLowerCase()) {
2189
- case 'together':
2190
- case 'forwards':
2191
- case 'backwards': {
2192
- warning(
2193
- false,
2194
- '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2195
- 'Use lowercase "%s" instead.',
2196
- revealOrder,
2197
- revealOrder.toLowerCase(),
2198
- );
2199
- break;
2200
- }
2201
- case 'forward':
2202
- case 'backward': {
2203
- warning(
2204
- false,
2205
- '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2206
- 'React uses the -s suffix in the spelling. Use "%ss" instead.',
2207
- revealOrder,
2208
- revealOrder.toLowerCase(),
2209
- );
2210
- break;
2211
- }
2212
- default:
2213
- warning(
2214
- false,
2215
- '"%s" is not a supported revealOrder on <SuspenseList />. ' +
2216
- 'Did you mean "together", "forwards" or "backwards"?',
2217
- revealOrder,
2218
- );
2219
- break;
2220
- }
2221
- } else {
2222
- warning(
2223
- false,
2224
- '%s is not a supported value for revealOrder on <SuspenseList />. ' +
2225
- 'Did you mean "together", "forwards" or "backwards"?',
2226
- revealOrder,
2227
- );
2228
- }
2229
- }
2199
+ case 'together': {
2200
+ initSuspenseListRenderState(
2201
+ workInProgress,
2202
+ false, // isBackwards
2203
+ null, // tail
2204
+ null, // last
2205
+ );
2206
+ break;
2207
+ }
2208
+ default: {
2209
+ // The default reveal order is the same as not having
2210
+ // a boundary.
2211
+ workInProgress.memoizedState = null;
2212
}
2231
- // We mark this as having captured but it really just says to the
2232
- // complete phase that we should treat this as done, whatever form
2233
- // it is in. No need for a second pass.
2234
- workInProgress.effectTag |= DidCapture;
2213
}
2214
}
2237
- workInProgress.memoizedState = suspenseListState;
2238
- workInProgress.child = nextChildFibers;
2239
- return nextChildFibers;
2215
+ return workInProgress.child;
2216
}
2217
2218
function updatePortalComponent(
2633
break;
2634
}
2635
case SuspenseListComponent: {
2660
- // Check if the children have any pending work.
2636
+ const didSuspendBefore =
2637
+ (current.effectTag & DidCapture) !== NoEffect;
2638
+
2639
const childExpirationTime = workInProgress.childExpirationTime;
2640
if (childExpirationTime < renderExpirationTime) {
2641
+ // If none of the children had any work, that means that none of
2642
+ // them got retried so they'll still be blocked in the same way
2643
+ // as before. We can fast bail out.
2644
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
2664
- // None of the children have any work, so we can do a fast bailout.
2645
+ if (didSuspendBefore) {
2646
+ workInProgress.effectTag |= DidCapture;
2647
+ }
2648
return null;
2649
}
2667
- // Try the normal path.
2668
- return updateSuspenseListComponent(
2669
- current,
2670
- workInProgress,
2671
- renderExpirationTime,
2672
- );
2650
+
2651
+ if (didSuspendBefore) {
2652
+ // If something was in fallback state last time, and we have all the
2653
+ // same children then we're still in progressive loading state.
2654
+ // Something might get unblocked by state updates or retries in the
2655
+ // tree which will affect the tail. So we need to use the normal
2656
+ // path to compute the correct tail.
2657
+ return updateSuspenseListComponent(
2658
+ current,
2659
+ workInProgress,
2660
+ renderExpirationTime,
2661
+ );
2662
+ }
2663
+
2664
+ // If nothing suspended before and we're rendering the same children,
2665
+ // then the tail doesn't matter. Anything new that suspends will work
2666
+ // in the "together" mode, so we can continue from the state we had.
2667
+ let renderState = workInProgress.memoizedState;
2668
+ if (renderState !== null) {
2669
+ // Reset to the "together" mode in case we've started a different
2670
+ // update in the past but didn't complete it.
2671
+ renderState.rendering = null;
2672
+ renderState.tail = null;
2673
+ }
2674
+ pushSuspenseContext(workInProgress, suspenseStackCursor.current);
2675
+ break;
2676
}
2677
case EventComponent:
2678
if (enableFlareAPI) {