Skip to content

Commit 26cf280

Browse files
authored
Switch the default revealOrder to "forwards" and tail "hidden" on SuspenseList (facebook#35018)
We have warned about this for a while now so we can make the switch. Often when you reach for SuspenseList, you mean forwards. It doesn't make sense to have the default to just be a noop. While "together" is another useful mode that's more like a Group so isn't so associated with the default as List. So we're switching it. However, tail=hidden isn't as obvious of a default it does allow for a convenient pattern for streaming in list of items by default. This doesn't yet switch the rendering order of "backwards". That's coming in a follow up.
1 parent c9ddee7 commit 26cf280

File tree

7 files changed

+142
-125
lines changed

7 files changed

+142
-125
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe('ReactDOMFizzSuspenseList', () => {
134134
}
135135

136136
// @gate enableSuspenseList
137-
it('shows content independently by default', async () => {
137+
it('shows content forwards by default', async () => {
138138
const A = createAsyncText('A');
139139
const B = createAsyncText('B');
140140
const C = createAsyncText('C');
@@ -157,31 +157,38 @@ describe('ReactDOMFizzSuspenseList', () => {
157157
);
158158
}
159159

160-
await A.resolve();
160+
await C.resolve();
161161

162162
await serverAct(async () => {
163163
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
164164
pipe(writable);
165165
});
166166

167-
assertLog(['A', 'Suspend! [B]', 'Suspend! [C]', 'Loading B', 'Loading C']);
167+
assertLog([
168+
'Suspend! [A]',
169+
'Suspend! [B]', // TODO: Defer rendering the content after fallback if previous suspended,
170+
'C',
171+
'Loading A',
172+
'Loading B',
173+
'Loading C',
174+
]);
168175

169176
expect(getVisibleChildren(container)).toEqual(
170177
<div>
171-
<span>A</span>
178+
<span>Loading A</span>
172179
<span>Loading B</span>
173180
<span>Loading C</span>
174181
</div>,
175182
);
176183

177-
await serverAct(() => C.resolve());
178-
assertLog(['C']);
184+
await serverAct(() => A.resolve());
185+
assertLog(['A']);
179186

180187
expect(getVisibleChildren(container)).toEqual(
181188
<div>
182189
<span>A</span>
183190
<span>Loading B</span>
184-
<span>C</span>
191+
<span>Loading C</span>
185192
</div>,
186193
);
187194

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2104,7 +2104,8 @@ export function validateSuspenseListChildren(
21042104
) {
21052105
if (__DEV__) {
21062106
if (
2107-
(revealOrder === 'forwards' ||
2107+
(revealOrder == null ||
2108+
revealOrder === 'forwards' ||
21082109
revealOrder === 'backwards' ||
21092110
revealOrder === 'unstable_legacy-backwards') &&
21102111
children !== undefined &&

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 34 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3245,20 +3245,15 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32453245
if (__DEV__) {
32463246
const cacheKey = revealOrder == null ? 'null' : revealOrder;
32473247
if (
3248+
revealOrder != null &&
32483249
revealOrder !== 'forwards' &&
32493250
revealOrder !== 'unstable_legacy-backwards' &&
32503251
revealOrder !== 'together' &&
32513252
revealOrder !== 'independent' &&
32523253
!didWarnAboutRevealOrder[cacheKey]
32533254
) {
32543255
didWarnAboutRevealOrder[cacheKey] = true;
3255-
if (revealOrder == null) {
3256-
console.error(
3257-
'The default for the <SuspenseList revealOrder="..."> prop is changing. ' +
3258-
'To be future compatible you must explictly specify either ' +
3259-
'"independent" (the current default), "together", "forwards" or "legacy_unstable-backwards".',
3260-
);
3261-
} else if (revealOrder === 'backwards') {
3256+
if (revealOrder === 'backwards') {
32623257
console.error(
32633258
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. ' +
32643259
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
@@ -3314,18 +3309,7 @@ function validateTailOptions(
33143309
const cacheKey = tailMode == null ? 'null' : tailMode;
33153310
if (!didWarnAboutTailOptions[cacheKey]) {
33163311
if (tailMode == null) {
3317-
if (
3318-
revealOrder === 'forwards' ||
3319-
revealOrder === 'backwards' ||
3320-
revealOrder === 'unstable_legacy-backwards'
3321-
) {
3322-
didWarnAboutTailOptions[cacheKey] = true;
3323-
console.error(
3324-
'The default for the <SuspenseList tail="..."> prop is changing. ' +
3325-
'To be future compatible you must explictly specify either ' +
3326-
'"visible" (the current default), "collapsed" or "hidden".',
3327-
);
3328-
}
3312+
// The default tail is now "hidden".
33293313
} else if (
33303314
tailMode !== 'visible' &&
33313315
tailMode !== 'collapsed' &&
@@ -3338,14 +3322,15 @@ function validateTailOptions(
33383322
tailMode,
33393323
);
33403324
} else if (
3325+
revealOrder != null &&
33413326
revealOrder !== 'forwards' &&
33423327
revealOrder !== 'backwards' &&
33433328
revealOrder !== 'unstable_legacy-backwards'
33443329
) {
33453330
didWarnAboutTailOptions[cacheKey] = true;
33463331
console.error(
33473332
'<SuspenseList tail="%s" /> is only valid if revealOrder is ' +
3348-
'"forwards" or "backwards". ' +
3333+
'"forwards" (default) or "backwards". ' +
33493334
'Did you mean to specify revealOrder="forwards"?',
33503335
tailMode,
33513336
);
@@ -3449,30 +3434,6 @@ function updateSuspenseListComponent(
34493434
workInProgress.memoizedState = null;
34503435
} else {
34513436
switch (revealOrder) {
3452-
case 'forwards': {
3453-
const lastContentRow = findLastContentRow(workInProgress.child);
3454-
let tail;
3455-
if (lastContentRow === null) {
3456-
// The whole list is part of the tail.
3457-
// TODO: We could fast path by just rendering the tail now.
3458-
tail = workInProgress.child;
3459-
workInProgress.child = null;
3460-
} else {
3461-
// Disconnect the tail rows after the content row.
3462-
// We're going to render them separately later.
3463-
tail = lastContentRow.sibling;
3464-
lastContentRow.sibling = null;
3465-
}
3466-
initSuspenseListRenderState(
3467-
workInProgress,
3468-
false, // isBackwards
3469-
tail,
3470-
lastContentRow,
3471-
tailMode,
3472-
treeForkCount,
3473-
);
3474-
break;
3475-
}
34763437
case 'backwards':
34773438
case 'unstable_legacy-backwards': {
34783439
// We're going to find the first row that has existing content.
@@ -3517,10 +3478,37 @@ function updateSuspenseListComponent(
35173478
);
35183479
break;
35193480
}
3520-
default: {
3521-
// The default reveal order is the same as not having
3481+
case 'independent': {
3482+
// The "independent" reveal order is the same as not having
35223483
// a boundary.
35233484
workInProgress.memoizedState = null;
3485+
break;
3486+
}
3487+
// The default is now forwards.
3488+
case 'forwards':
3489+
default: {
3490+
const lastContentRow = findLastContentRow(workInProgress.child);
3491+
let tail;
3492+
if (lastContentRow === null) {
3493+
// The whole list is part of the tail.
3494+
// TODO: We could fast path by just rendering the tail now.
3495+
tail = workInProgress.child;
3496+
workInProgress.child = null;
3497+
} else {
3498+
// Disconnect the tail rows after the content row.
3499+
// We're going to render them separately later.
3500+
tail = lastContentRow.sibling;
3501+
lastContentRow.sibling = null;
3502+
}
3503+
initSuspenseListRenderState(
3504+
workInProgress,
3505+
false, // isBackwards
3506+
tail,
3507+
lastContentRow,
3508+
tailMode,
3509+
treeForkCount,
3510+
);
3511+
break;
35243512
}
35253513
}
35263514
}

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,11 @@ function cutOffTailIfNeeded(
698698
return;
699699
}
700700
switch (renderState.tailMode) {
701-
case 'hidden': {
701+
case 'visible': {
702+
// Everything should remain as it was.
703+
break;
704+
}
705+
case 'collapsed': {
702706
// Any insertions at the end of the tail list after this point
703707
// should be invisible. If there are already mounted boundaries
704708
// anything before them are not considered for collapsing.
@@ -716,15 +720,23 @@ function cutOffTailIfNeeded(
716720
// last rendered item.
717721
if (lastTailNode === null) {
718722
// All remaining items in the tail are insertions.
719-
renderState.tail = null;
723+
if (!hasRenderedATailFallback && renderState.tail !== null) {
724+
// We suspended during the head. We want to show at least one
725+
// row at the tail. So we'll keep on and cut off the rest.
726+
renderState.tail.sibling = null;
727+
} else {
728+
renderState.tail = null;
729+
}
720730
} else {
721731
// Detach the insertion after the last node that was already
722732
// inserted.
723733
lastTailNode.sibling = null;
724734
}
725735
break;
726736
}
727-
case 'collapsed': {
737+
// Hidden is now the default.
738+
case 'hidden':
739+
default: {
728740
// Any insertions at the end of the tail list after this point
729741
// should be invisible. If there are already mounted boundaries
730742
// anything before them are not considered for collapsing.
@@ -742,13 +754,7 @@ function cutOffTailIfNeeded(
742754
// last rendered item.
743755
if (lastTailNode === null) {
744756
// All remaining items in the tail are insertions.
745-
if (!hasRenderedATailFallback && renderState.tail !== null) {
746-
// We suspended during the head. We want to show at least one
747-
// row at the tail. So we'll keep on and cut off the rest.
748-
renderState.tail.sibling = null;
749-
} else {
750-
renderState.tail = null;
751-
}
757+
renderState.tail = null;
752758
} else {
753759
// Detach the insertion after the last node that was already
754760
// inserted.
@@ -1795,7 +1801,8 @@ function completeWork(
17951801
// This might have been modified.
17961802
if (
17971803
renderState.tail === null &&
1798-
renderState.tailMode === 'hidden' &&
1804+
renderState.tailMode !== 'collapsed' &&
1805+
renderState.tailMode !== 'visible' &&
17991806
!renderedTail.alternate &&
18001807
!getIsHydrating() // We don't cut it if we're hydrating.
18011808
) {

packages/react-reconciler/src/ReactFiberSuspenseComponent.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ export function findFirstSuspended(row: Fiber): null | Fiber {
7979
node.tag === SuspenseListComponent &&
8080
// Independent revealOrder can't be trusted because it doesn't
8181
// keep track of whether it suspended or not.
82-
(node.memoizedProps.revealOrder === 'forwards' ||
83-
node.memoizedProps.revealOrder === 'backwards' ||
84-
node.memoizedProps.revealOrder === 'unstable_legacy-backwards' ||
85-
node.memoizedProps.revealOrder === 'together')
82+
node.memoizedProps.revealOrder !== 'independent'
8683
) {
8784
const didSuspend = (node.flags & DidCapture) !== NoFlags;
8885
if (didSuspend) {

0 commit comments

Comments
 (0)