Skip to content

Commit 5cb8f6f

Browse files
authored
Add tail="collapsed" option to SuspenseList (facebook#16007)
* Add tail="collapsed" option * Fix issue with tail exceeding the CPU time limit We used to assume that this didn't suspend but this branch happens in both cases. This fixes it so that we first check if we suspended. Now we can fix the tail so that it always render an additional fallback in this scenario.
1 parent 46bd11a commit 5cb8f6f

File tree

4 files changed

+659
-4
lines changed

4 files changed

+659
-4
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
1414
import type {
1515
SuspenseState,
1616
SuspenseListRenderState,
17+
SuspenseListTailMode,
1718
} from './ReactFiberSuspenseComponent';
1819
import type {SuspenseContext} from './ReactFiberSuspenseContext';
1920

@@ -188,6 +189,7 @@ let didWarnAboutFunctionRefs;
188189
export let didWarnAboutReassigningProps;
189190
let didWarnAboutMaxDuration;
190191
let didWarnAboutRevealOrder;
192+
let didWarnAboutTailOptions;
191193

192194
if (__DEV__) {
193195
didWarnAboutBadClass = {};
@@ -198,6 +200,7 @@ if (__DEV__) {
198200
didWarnAboutReassigningProps = false;
199201
didWarnAboutMaxDuration = false;
200202
didWarnAboutRevealOrder = {};
203+
didWarnAboutTailOptions = {};
201204
}
202205

203206
export function reconcileChildren(
@@ -2063,11 +2066,40 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
20632066
}
20642067
}
20652068

2069+
function validateTailOptions(
2070+
tailMode: SuspenseListTailMode,
2071+
revealOrder: SuspenseListRevealOrder,
2072+
) {
2073+
if (__DEV__) {
2074+
if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) {
2075+
if (tailMode !== 'collapsed') {
2076+
didWarnAboutTailOptions[tailMode] = true;
2077+
warning(
2078+
false,
2079+
'"%s" is not a supported value for tail on <SuspenseList />. ' +
2080+
'Did you mean "collapsed"?',
2081+
tailMode,
2082+
);
2083+
} else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') {
2084+
didWarnAboutTailOptions[tailMode] = true;
2085+
warning(
2086+
false,
2087+
'<SuspenseList tail="%s" /> is only valid if revealOrder is ' +
2088+
'"forwards" or "backwards". ' +
2089+
'Did you mean to specify revealOrder="forwards"?',
2090+
tailMode,
2091+
);
2092+
}
2093+
}
2094+
}
2095+
}
2096+
20662097
function initSuspenseListRenderState(
20672098
workInProgress: Fiber,
20682099
isBackwards: boolean,
20692100
tail: null | Fiber,
20702101
lastContentRow: null | Fiber,
2102+
tailMode: SuspenseListTailMode,
20712103
): void {
20722104
let renderState: null | SuspenseListRenderState =
20732105
workInProgress.memoizedState;
@@ -2078,6 +2110,7 @@ function initSuspenseListRenderState(
20782110
last: lastContentRow,
20792111
tail: tail,
20802112
tailExpiration: 0,
2113+
tailMode: tailMode,
20812114
};
20822115
} else {
20832116
// We can reuse the existing object from previous renders.
@@ -2086,6 +2119,7 @@ function initSuspenseListRenderState(
20862119
renderState.last = lastContentRow;
20872120
renderState.tail = tail;
20882121
renderState.tailExpiration = 0;
2122+
renderState.tailMode = tailMode;
20892123
}
20902124
}
20912125

@@ -2103,9 +2137,11 @@ function updateSuspenseListComponent(
21032137
) {
21042138
const nextProps = workInProgress.pendingProps;
21052139
const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
2140+
const tailMode: SuspenseListTailMode = nextProps.tail;
21062141
const newChildren = nextProps.children;
21072142

21082143
validateRevealOrder(revealOrder);
2144+
validateTailOptions(tailMode, revealOrder);
21092145

21102146
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
21112147

@@ -2163,6 +2199,7 @@ function updateSuspenseListComponent(
21632199
false, // isBackwards
21642200
tail,
21652201
lastContentRow,
2202+
tailMode,
21662203
);
21672204
break;
21682205
}
@@ -2193,6 +2230,7 @@ function updateSuspenseListComponent(
21932230
true, // isBackwards
21942231
tail,
21952232
null, // last
2233+
tailMode,
21962234
);
21972235
break;
21982236
}
@@ -2202,6 +2240,7 @@ function updateSuspenseListComponent(
22022240
false, // isBackwards
22032241
null, // tail
22042242
null, // last
2243+
undefined,
22052244
);
22062245
break;
22072246
}

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,46 @@ if (supportsMutation) {
537537
};
538538
}
539539

540+
function cutOffTailIfNeeded(
541+
renderState: SuspenseListRenderState,
542+
hasRenderedATailFallback: boolean,
543+
) {
544+
switch (renderState.tailMode) {
545+
case 'collapsed': {
546+
// Any insertions at the end of the tail list after this point
547+
// should be invisible. If there are already mounted boundaries
548+
// anything before them are not considered for collapsing.
549+
// Therefore we need to go through the whole tail to find if
550+
// there are any.
551+
let tailNode = renderState.tail;
552+
let lastTailNode = null;
553+
while (tailNode !== null) {
554+
if (tailNode.alternate !== null) {
555+
lastTailNode = tailNode;
556+
}
557+
tailNode = tailNode.sibling;
558+
}
559+
// Next we're simply going to delete all insertions after the
560+
// last rendered item.
561+
if (lastTailNode === null) {
562+
// All remaining items in the tail are insertions.
563+
if (!hasRenderedATailFallback && renderState.tail !== null) {
564+
// We suspended during the head. We want to show at least one
565+
// row at the tail. So we'll keep on and cut off the rest.
566+
renderState.tail.sibling = null;
567+
} else {
568+
renderState.tail = null;
569+
}
570+
} else {
571+
// Detach the insertion after the last node that was already
572+
// inserted.
573+
lastTailNode.sibling = null;
574+
}
575+
break;
576+
}
577+
}
578+
}
579+
540580
// Note this, might mutate the workInProgress passed in.
541581
function hasSuspendedChildrenAndNewContent(
542582
workInProgress: Fiber,
@@ -991,11 +1031,18 @@ function completeWork(
9911031
didSuspendAlready =
9921032
(workInProgress.effectTag & DidCapture) !== NoEffect;
9931033
}
1034+
if (didSuspendAlready) {
1035+
cutOffTailIfNeeded(renderState, false);
1036+
}
9941037
// Next we're going to render the tail.
9951038
} else {
9961039
// Append the rendered row to the child list.
9971040
if (!didSuspendAlready) {
998-
if (
1041+
if (isShowingAnyFallbacks(renderedTail)) {
1042+
workInProgress.effectTag |= DidCapture;
1043+
didSuspendAlready = true;
1044+
cutOffTailIfNeeded(renderState, true);
1045+
} else if (
9991046
now() > renderState.tailExpiration &&
10001047
renderExpirationTime > Never
10011048
) {
@@ -1004,6 +1051,9 @@ function completeWork(
10041051
// The assumption is that this is usually faster.
10051052
workInProgress.effectTag |= DidCapture;
10061053
didSuspendAlready = true;
1054+
1055+
cutOffTailIfNeeded(renderState, false);
1056+
10071057
// Since nothing actually suspended, there will nothing to ping this
10081058
// to get it started back up to attempt the next item. If we can show
10091059
// them, then they really have the same priority as this render.
@@ -1015,9 +1065,6 @@ function completeWork(
10151065
if (enableSchedulerTracing) {
10161066
markSpawnedWork(nextPriority);
10171067
}
1018-
} else if (isShowingAnyFallbacks(renderedTail)) {
1019-
workInProgress.effectTag |= DidCapture;
1020-
didSuspendAlready = true;
10211068
}
10221069
}
10231070
if (renderState.isBackwards) {

packages/react-reconciler/src/ReactFiberSuspenseComponent.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {SuspenseComponent} from 'shared/ReactWorkTags';
1414
// Alternatively we can make this use an effect tag similar to SuspenseList.
1515
export type SuspenseState = {||};
1616

17+
export type SuspenseListTailMode = 'collapsed' | void;
18+
1719
export type SuspenseListRenderState = {|
1820
isBackwards: boolean,
1921
// The currently rendering tail row.
@@ -24,6 +26,8 @@ export type SuspenseListRenderState = {|
2426
tail: null | Fiber,
2527
// The absolute time in ms that we'll expire the tail rendering.
2628
tailExpiration: number,
29+
// Tail insertions setting.
30+
tailMode: SuspenseListTailMode,
2731
|};
2832

2933
export function shouldCaptureSuspense(

0 commit comments

Comments
 (0)