Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Render children passed to "backwards" SuspenseList in reverse mount o… · react/react@488d88b · GitHub
Skip to content

Commit 488d88b

Browse files
authored
Render children passed to "backwards" SuspenseList in reverse mount order (#35021)
Stacked on #35018. This mounts the children of SuspenseList backwards. Meaning the first child is mounted last in the DOM (and effect list). It's like calling reverse() on the children. This is meant to set us up for allowing AsyncIterable children where the unknown number of children streams in at the end (which is the beginning in a backwards SuspenseList). For consistency we do that with other children too. `unstable_legacy-backwards` still exists for the old mode but is meant to be deprecated. <img width="100" alt="image" src="https://github.com/user-attachments/assets/5c2a95d7-34c4-4a4e-b602-3646a834d779" />
1 parent 26cf280 commit 488d88b

5 files changed

Lines changed: 140 additions & 33 deletions

File tree

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,77 @@ describe('ReactDOMFizzSuspenseList', () => {
656656
);
657657
});
658658

659+
// @gate enableSuspenseList
660+
it('displays each items in "backwards" mount order',async()=>{
661+
constA=createAsyncText('A');
662+
constB=createAsyncText('B');
663+
constC=createAsyncText('C');
664+
665+
functionFoo(){
666+
return(
667+
<div>
668+
<SuspenseListrevealOrder="backwards"tail="visible">
669+
<Suspensefallback={<Texttext="Loading C"/>}>
670+
<C/>
671+
</Suspense>
672+
<Suspensefallback={<Texttext="Loading B"/>}>
673+
<B/>
674+
</Suspense>
675+
<Suspensefallback={<Texttext="Loading A"/>}>
676+
<A/>
677+
</Suspense>
678+
</SuspenseList>
679+
</div>
680+
);
681+
}
682+
683+
awaitA.resolve();
684+
685+
awaitserverAct(async()=>{
686+
const{pipe}=ReactDOMFizzServer.renderToPipeableStream(<Foo/>);
687+
pipe(writable);
688+
});
689+
690+
assertLog([
691+
'Suspend! [C]',
692+
'Suspend! [B]',// TODO: Defer rendering the content after fallback if previous suspended,
693+
'A',
694+
'Loading C',
695+
'Loading B',
696+
'Loading A',
697+
]);
698+
699+
expect(getVisibleChildren(container)).toEqual(
700+
<div>
701+
<span>Loading A</span>
702+
<span>Loading B</span>
703+
<span>Loading C</span>
704+
</div>,
705+
);
706+
707+
awaitserverAct(()=>C.resolve());
708+
assertLog(['C']);
709+
710+
expect(getVisibleChildren(container)).toEqual(
711+
<div>
712+
<span>Loading A</span>
713+
<span>Loading B</span>
714+
<span>C</span>
715+
</div>,
716+
);
717+
718+
awaitserverAct(()=>B.resolve());
719+
assertLog(['B']);
720+
721+
expect(getVisibleChildren(container)).toEqual(
722+
<div>
723+
<span>A</span>
724+
<span>B</span>
725+
<span>C</span>
726+
</div>,
727+
);
728+
});
729+
659730
// @gate enableSuspenseList
660731
it('displays each items in "backwards" order in legacy mode',async()=>{
661732
constA=createAsyncText('A');
@@ -737,15 +808,13 @@ describe('ReactDOMFizzSuspenseList', () => {
737808
return(
738809
<div>
739810
<SuspenseListrevealOrder="forwards"tail="visible">
740-
<SuspenseList
741-
revealOrder="unstable_legacy-backwards"
742-
tail="visible">
743-
<Suspensefallback={<Texttext="Loading A"/>}>
744-
<A/>
745-
</Suspense>
811+
<SuspenseListrevealOrder="backwards"tail="visible">
746812
<Suspensefallback={<Texttext="Loading B"/>}>
747813
<B/>
748814
</Suspense>
815+
<Suspensefallback={<Texttext="Loading A"/>}>
816+
<A/>
817+
</Suspense>
749818
</SuspenseList>
750819
<Suspensefallback={<Texttext="Loading C"/>}>
751820
<C/>

‎packages/react-reconciler/src/ReactFiberBeginWork.js‎

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,18 +3247,14 @@ function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
32473247
if(
32483248
revealOrder!=null&&
32493249
revealOrder!=='forwards'&&
3250+
revealOrder!=='backwards'&&
32503251
revealOrder!=='unstable_legacy-backwards'&&
32513252
revealOrder!=='together'&&
32523253
revealOrder!=='independent'&&
32533254
!didWarnAboutRevealOrder[cacheKey]
32543255
){
32553256
didWarnAboutRevealOrder[cacheKey]=true;
3256-
if(revealOrder==='backwards'){
3257-
console.error(
3258-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
3259-
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
3260-
);
3261-
}elseif(typeofrevealOrder==='string'){
3257+
if(typeofrevealOrder==='string'){
32623258
switch(revealOrder.toLowerCase()){
32633259
case'together':
32643260
case'forwards':
@@ -3371,6 +3367,17 @@ function initSuspenseListRenderState(
33713367
}
33723368
}
33733369

3370+
functionreverseChildren(fiber: Fiber): void{
3371+
letrow=fiber.child;
3372+
fiber.child=null;
3373+
while(row!==null){
3374+
constnextRow=row.sibling;
3375+
row.sibling=fiber.child;
3376+
fiber.child=row;
3377+
row=nextRow;
3378+
}
3379+
}
3380+
33743381
// This can end up rendering this component multiple passes.
33753382
// The first pass splits the children fibers into two sets. A head and tail.
33763383
// We first render the head. If anything is in fallback state, we do another
@@ -3409,7 +3416,16 @@ function updateSuspenseListComponent(
34093416
validateTailOptions(tailMode,revealOrder);
34103417
validateSuspenseListChildren(newChildren,revealOrder);
34113418

3412-
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3419+
if(revealOrder==='backwards'&&current!==null){
3420+
// For backwards the current mounted set will be backwards. Reconciling against it
3421+
// will lead to mismatches and reorders. We need to swap the original set first
3422+
// and then restore it afterwards.
3423+
reverseChildren(current);
3424+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3425+
reverseChildren(current);
3426+
}else{
3427+
reconcileChildren(current,workInProgress,newChildren,renderLanes);
3428+
}
34133429
// Read how many children forks this set pushed so we can push it every time we retry.
34143430
consttreeForkCount=getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
34153431

@@ -3434,7 +3450,37 @@ function updateSuspenseListComponent(
34343450
workInProgress.memoizedState=null;
34353451
}else{
34363452
switch(revealOrder){
3437-
case'backwards':
3453+
case'backwards': {
3454+
// We're going to find the first row that has existing content.
3455+
// We are also going to reverse the order of anything in the existing content
3456+
// since we want to actually render them backwards from the reconciled set.
3457+
// The tail is left in order, because it'll be added to the front as we
3458+
// complete each item.
3459+
constlastContentRow=findLastContentRow(workInProgress.child);
3460+
lettail;
3461+
if(lastContentRow===null){
3462+
// The whole list is part of the tail.
3463+
tail=workInProgress.child;
3464+
workInProgress.child=null;
3465+
}else{
3466+
// Disconnect the tail rows after the content row.
3467+
// We're going to render them separately later in reverse order.
3468+
tail=lastContentRow.sibling;
3469+
lastContentRow.sibling=null;
3470+
// We have to now reverse the main content so it renders backwards too.
3471+
reverseChildren(workInProgress);
3472+
}
3473+
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
3474+
initSuspenseListRenderState(
3475+
workInProgress,
3476+
true,// isBackwards
3477+
tail,
3478+
null,// last
3479+
tailMode,
3480+
treeForkCount,
3481+
);
3482+
break;
3483+
}
34383484
case'unstable_legacy-backwards': {
34393485
// We're going to find the first row that has existing content.
34403486
// At the same time we're going to reverse the list of everything

‎packages/react-reconciler/src/ReactFiberCompleteWork.js‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,10 +1838,6 @@ function completeWork(
18381838
}
18391839
}
18401840
if(renderState.isBackwards){
1841-
// The effect list of the backwards tail will have been added
1842-
// to the end. This breaks the guarantee that life-cycles fire in
1843-
// sibling order but that isn't a strong guarantee promised by React.
1844-
// Especially since these might also just pop in during future commits.
18451841
// Append to the beginning of the list.
18461842
renderedTail.sibling=workInProgress.child;
18471843
workInProgress.child=renderedTail;

‎packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,22 +1022,22 @@ describe('ReactSuspenseList', () => {
10221022
});
10231023

10241024
// @gate enableSuspenseList
1025-
it('warns if revealOrder="backwards" is specified',async()=>{
1025+
it('displays each items in "backwards" order',async()=>{
10261026
constA=createAsyncText('A');
10271027
constB=createAsyncText('B');
10281028
constC=createAsyncText('C');
10291029

10301030
functionFoo(){
10311031
return(
10321032
<SuspenseListrevealOrder="backwards"tail="visible">
1033-
<Suspensefallback={<Texttext="Loading A"/>}>
1034-
<A/>
1033+
<Suspensefallback={<Texttext="Loading C"/>}>
1034+
<C/>
10351035
</Suspense>
10361036
<Suspensefallback={<Texttext="Loading B"/>}>
10371037
<B/>
10381038
</Suspense>
1039-
<Suspensefallback={<Texttext="Loading C"/>}>
1040-
<C/>
1039+
<Suspensefallback={<Texttext="Loading A"/>}>
1040+
<A/>
10411041
</Suspense>
10421042
</SuspenseList>
10431043
);
@@ -1056,14 +1056,6 @@ describe('ReactSuspenseList', () => {
10561056
'Suspend! [C]',
10571057
]);
10581058

1059-
assertConsoleErrorDev([
1060-
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. '+
1061-
'To be future compatible you must specify '+
1062-
'revealOrder="legacy_unstable-backwards" instead.'+
1063-
'\n in SuspenseList (at **)'+
1064-
'\n in Foo (at **)',
1065-
]);
1066-
10671059
expect(ReactNoop).toMatchRenderedOutput(
10681060
<>
10691061
<span>Loading A</span>
@@ -1101,7 +1093,7 @@ describe('ReactSuspenseList', () => {
11011093
});
11021094

11031095
// @gate enableSuspenseList
1104-
it('displays each items in "backwards" order',async()=>{
1096+
it('displays each items in "backwards" order (legacy)',async()=>{
11051097
constA=createAsyncText('A');
11061098
constB=createAsyncText('B');
11071099
constC=createAsyncText('C');

‎packages/react-server/src/ReactFizzServer.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,11 @@ function renderSuspenseListRows(
20172017
constparentSegment=task.blockedSegment;
20182018
constchildIndex=parentSegment.children.length;
20192019
constinsertionIndex=parentSegment.chunks.length;
2020-
for(leti=totalChildren-1;i>=0;i--){
2020+
for(letn=0;n<totalChildren;n++){
2021+
consti=
2022+
revealOrder==='unstable_legacy-backwards'
2023+
? totalChildren-1-n
2024+
: n;
20212025
constnode=rows[i];
20222026
task.row=previousSuspenseListRow=createSuspenseListRow(
20232027
previousSuspenseListRow,

0 commit comments

Comments
 (0)