@samitouri / QOS-React-2 / commits / 270dc2e4de

Add forwards and backwards options to SuspenseList (#15918)

* Add forwards option * Add backwards option * Add comment * Add customized warning messages for case and typos * Add some more tests for insertions and updates in start/middle/end

Sebastian Markbåge committed Jun 20, 2019 at 11:03 UTC 270dc2e4debe8c198ce6dc9200ab72df4ee4926b
4 files changed +734 -29
packages/react-reconciler/src/ReactFiberBeginWork.js
+149 -8
@@ -127,6 +127,7 @@ import {
127 addSubtreeSuspenseContext,
128 setShallowSuspenseContext,
129 } from './ReactFiberSuspenseContext';
130 +import {isShowingAnyFallbacks} from './ReactFiberSuspenseComponent';
131 import {
132 pushProvider,
133 propagateContextChange,
@@ -1983,8 +1984,36 @@ function propagateSuspenseContextChange(
1984 }
1985 }
1986
1986 -type SuspenseListRevealOrder = 'together' | void;
1987 +function findLastContentRow(firstChild: null | Fiber): null | Fiber {
1988 + // This is going to find the last row among these children that is already
1989 + // showing content on the screen, as opposed to being in fallback state or
1990 + // new. If a row has multiple Suspense boundaries, any of them being in the
1991 + // fallback state, counts as the whole row being in a fallback state.
1992 + // Note that the "rows" will be workInProgress, but any nested children
1993 + // will still be current since we haven't rendered them yet. The mounted
1994 + // order may not be the same as the new order. We use the new order.
1995 + let row = firstChild;
1996 + let lastContentRow: null | Fiber = null;
1997 + while (row !== null) {
1998 + let currentRow = row.alternate;
1999 + // New rows can't be content rows.
2000 + if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
2001 + lastContentRow = row;
2002 + }
2003 + row = row.sibling;
2004 + }
2005 + return lastContentRow;
2006 +}
2007 +
2008 +type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void;
2009
2010 +// This can end up rendering this component multiple passes.
2011 +// The first pass splits the children fibers into two sets. A head and tail.
2012 +// We first render the head. If anything is in fallback state, we do another
2013 +// pass through beginWork to rerender all children (including the tail) with
2014 +// the force suspend context. If the first render didn't have anything in
2015 +// in fallback state. Then we render each row in the tail one-by-one.
2016 +// That happens in the completeWork phase without going back to beginWork.
2017 function updateSuspenseListComponent(
2018 current: Fiber | null,
2019 workInProgress: Fiber,
@@ -2033,6 +2062,10 @@ function updateSuspenseListComponent(
2062 );
2063 suspenseListState = {
2064 didSuspend: true,
2065 + isBackwards: false,
2066 + rendering: null,
2067 + last: null,
2068 + tail: null,
2069 };
2070 } else {
2071 let didForceFallback =
@@ -2063,7 +2096,79 @@ function updateSuspenseListComponent(
2096 }
2097
2098 switch (revealOrder) {
2066 - // TODO: For other reveal orders we'll need to split the nextChildFibers set.
2099 + case 'forwards': {
2100 + // If need to force fallbacks in this pass we're just going to
2101 + // force the whole set to suspend so we don't have to do anything
2102 + // further here.
2103 + if (!shouldForceFallback) {
2104 + let lastContentRow = findLastContentRow(nextChildFibers);
2105 + let tail;
2106 + if (lastContentRow === null) {
2107 + // The whole list is part of the tail.
2108 + // TODO: We could fast path by just rendering the tail now.
2109 + tail = nextChildFibers;
2110 + nextChildFibers = null;
2111 + } else {
2112 + // Disconnect the tail rows after the content row.
2113 + // We're going to render them separately later.
2114 + tail = lastContentRow.sibling;
2115 + lastContentRow.sibling = null;
2116 + }
2117 + if (suspenseListState === null) {
2118 + suspenseListState = {
2119 + didSuspend: false,
2120 + isBackwards: false,
2121 + rendering: null,
2122 + last: lastContentRow,
2123 + tail: tail,
2124 + };
2125 + } else {
2126 + suspenseListState.tail = tail;
2127 + }
2128 + }
2129 + break;
2130 + }
2131 + case 'backwards': {
2132 + // If need to force fallbacks in this pass we're just going to
2133 + // force the whole set to suspend so we don't have to do anything
2134 + // further here.
2135 + if (!shouldForceFallback) {
2136 + // We're going to find the first row that has existing content.
2137 + // At the same time we're going to reverse the list of everything
2138 + // we pass in the meantime. That's going to be our tail in reverse
2139 + // order.
2140 + let tail = null;
2141 + let row = nextChildFibers;
2142 + nextChildFibers = null;
2143 + while (row !== null) {
2144 + let currentRow = row.alternate;
2145 + // New rows can't be content rows.
2146 + if (currentRow !== null && !isShowingAnyFallbacks(currentRow)) {
2147 + // This is the beginning of the main content.
2148 + nextChildFibers = row;
2149 + break;
2150 + }
2151 + let nextRow = row.sibling;
2152 + row.sibling = tail;
2153 + tail = row;
2154 + row = nextRow;
2155 + }
2156 + // TODO: If nextChildFibers is null, we can continue on the tail immediately.
2157 + if (suspenseListState === null) {
2158 + suspenseListState = {
2159 + didSuspend: false,
2160 + isBackwards: true,
2161 + rendering: null,
2162 + last: null,
2163 + tail: tail,
2164 + };
2165 + } else {
2166 + suspenseListState.isBackwards = true;
2167 + suspenseListState.tail = tail;
2168 + }
2169 + }
2170 + break;
2171 + }
2172 case 'together': {
2173 break;
2174 }
@@ -2076,12 +2181,48 @@ function updateSuspenseListComponent(
2181 !didWarnAboutRevealOrder[revealOrder]
2182 ) {
2183 didWarnAboutRevealOrder[revealOrder] = true;
2079 - warning(
2080 - false,
2081 - '"%s" is not a supported revealOrder on <SuspenseList />. ' +
2082 - 'Did you mean "together"?',
2083 - revealOrder,
2084 - );
2184 + if (typeof revealOrder === 'string') {
2185 + switch (revealOrder.toLowerCase()) {
2186 + case 'together':
2187 + case 'forwards':
2188 + case 'backwards': {
2189 + warning(
2190 + false,
2191 + '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2192 + 'Use lowercase "%s" instead.',
2193 + revealOrder,
2194 + revealOrder.toLowerCase(),
2195 + );
2196 + break;
2197 + }
2198 + case 'forward':
2199 + case 'backward': {
2200 + warning(
2201 + false,
2202 + '"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
2203 + 'React uses the -s suffix in the spelling. Use "%ss" instead.',
2204 + revealOrder,
2205 + revealOrder.toLowerCase(),
2206 + );
2207 + break;
2208 + }
2209 + default:
2210 + warning(
2211 + false,
2212 + '"%s" is not a supported revealOrder on <SuspenseList />. ' +
2213 + 'Did you mean "together", "forwards" or "backwards"?',
2214 + revealOrder,
2215 + );
2216 + break;
2217 + }
2218 + } else {
2219 + warning(
2220 + false,
2221 + '%s is not a supported value for revealOrder on <SuspenseList />. ' +
2222 + 'Did you mean "together", "forwards" or "backwards"?',
2223 + revealOrder,
2224 + );
2225 + }
2226 }
2227 }
2228 // We mark this as having captured but it really just says to the
packages/react-reconciler/src/ReactFiberCompleteWork.js
+83 -20
@@ -18,7 +18,10 @@ import type {
18 ChildSet,
19 } from './ReactFiberHostConfig';
20 import type {ReactEventComponentInstance} from 'shared/ReactTypes';
21 -import type {SuspenseState} from './ReactFiberSuspenseComponent';
21 +import type {
22 + SuspenseState,
23 + SuspenseListState,
24 +} from './ReactFiberSuspenseComponent';
25 import type {SuspenseContext} from './ReactFiberSuspenseContext';
26
27 import {
@@ -84,7 +87,12 @@ import {
87 InvisibleParentSuspenseContext,
88 hasSuspenseContext,
89 popSuspenseContext,
90 + pushSuspenseContext,
91 + setShallowSuspenseContext,
92 + ForceSuspenseFallback,
93 + setDefaultShallowSuspenseContext,
94 } from './ReactFiberSuspenseContext';
95 +import {isShowingAnyFallbacks} from './ReactFiberSuspenseComponent';
96 import {
97 isContextProvider as isLegacyContextProvider,
98 popContext as popLegacyContext,
@@ -917,25 +925,80 @@ function completeWork(
925 popSuspenseContext(workInProgress);
926
927 if ((workInProgress.effectTag & DidCapture) === NoEffect) {
920 - // This is the first pass. We need to figure out if anything is still
921 - // suspended in the rendered set.
922 - const renderedChildren = workInProgress.child;
923 - // If new content unsuspended, but there's still some content that
924 - // didn't. Then we need to do a second pass that forces everything
925 - // to keep showing their fallbacks.
926 - const needsRerender = hasSuspendedChildrenAndNewContent(
927 - workInProgress,
928 - renderedChildren,
929 - );
930 - if (needsRerender) {
931 - // Rerender the whole list, but this time, we'll force fallbacks
932 - // to stay in place.
933 - workInProgress.effectTag |= DidCapture;
934 - // Reset the effect list before doing the second pass since that's now invalid.
935 - workInProgress.firstEffect = workInProgress.lastEffect = null;
936 - // Schedule work so we know not to bail out.
937 - workInProgress.expirationTime = renderExpirationTime;
938 - return workInProgress;
928 + let suspenseListState: null | SuspenseListState =
929 + workInProgress.memoizedState;
930 + if (
931 + suspenseListState === null ||
932 + suspenseListState.rendering === null
933 + ) {
934 + // This is the first pass. We need to figure out if anything is still
935 + // suspended in the rendered set.
936 + const renderedChildren = workInProgress.child;
937 + // If new content unsuspended, but there's still some content that
938 + // didn't. Then we need to do a second pass that forces everything
939 + // to keep showing their fallbacks.
940 + const needsRerender = hasSuspendedChildrenAndNewContent(
941 + workInProgress,
942 + renderedChildren,
943 + );
944 + if (needsRerender) {
945 + // Rerender the whole list, but this time, we'll force fallbacks
946 + // to stay in place.
947 + workInProgress.effectTag |= DidCapture;
948 + // Reset the effect list before doing the second pass since that's now invalid.
949 + workInProgress.firstEffect = workInProgress.lastEffect = null;
950 + // Schedule work so we know not to bail out.
951 + workInProgress.expirationTime = renderExpirationTime;
952 + return workInProgress;
953 + }
954 + } else {
955 + // Append the rendered row to the child list.
956 + let rendered = suspenseListState.rendering;
957 + if (!suspenseListState.didSuspend) {
958 + suspenseListState.didSuspend = isShowingAnyFallbacks(rendered);
959 + }
960 + if (suspenseListState.isBackwards) {
961 + // The effect list of the backwards tail will have been added
962 + // to the end. This breaks the guarantee that life-cycles fire in
963 + // sibling order but that isn't a strong guarantee promised by React.
964 + // Especially since these might also just pop in during future commits.
965 + // Append to the beginning of the list.
966 + rendered.sibling = workInProgress.child;
967 + workInProgress.child = rendered;
968 + } else {
969 + let previousSibling = suspenseListState.last;
970 + if (previousSibling !== null) {
971 + previousSibling.sibling = rendered;
972 + } else {
973 + workInProgress.child = rendered;
974 + }
975 + suspenseListState.last = rendered;
976 + }
977 + }
978 +
979 + if (suspenseListState !== null && suspenseListState.tail !== null) {
980 + // We still have tail rows to render.
981 + // Pop a row.
982 + let next = suspenseListState.tail;
983 + suspenseListState.rendering = next;
984 + suspenseListState.tail = next.sibling;
985 + next.sibling = null;
986 +
987 + // Restore the context.
988 + // TODO: We can probably just avoid popping it instead and only
989 + // setting it the first time we go from not suspended to suspended.
990 + let suspenseContext = suspenseStackCursor.current;
991 + if (suspenseListState.didSuspend) {
992 + suspenseContext = setShallowSuspenseContext(
993 + suspenseContext,
994 + ForceSuspenseFallback,
995 + );
996 + } else {
997 + suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
998 + }
999 + pushSuspenseContext(workInProgress, suspenseContext);
1000 + // Do a pass over the next row.
1001 + return next;
1002 }
1003 } else {
1004 workInProgress.effectTag &= ~DidCapture;
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+36
@@ -8,12 +8,20 @@
8 */
9
10 import type {Fiber} from './ReactFiber';
11 +import {SuspenseComponent} from 'shared/ReactWorkTags';
12
13 // TODO: This is now an empty object. Should we switch this to a boolean?
14 export type SuspenseState = {||};
15
16 export type SuspenseListState = {|
17 didSuspend: boolean,
18 + isBackwards: boolean,
19 + // The currently rendering tail row.
20 + rendering: null | Fiber,
21 + // The last of the already rendered children.
22 + last: null | Fiber,
23 + // Remaining rows on the tail of the list.
24 + tail: null | Fiber,
25 |};
26
27 export function shouldCaptureSuspense(
@@ -43,3 +51,31 @@ export function shouldCaptureSuspense(
51 // If the parent is not able to handle it, we must handle it.
52 return true;
53 }
54 +
55 +export function isShowingAnyFallbacks(row: Fiber): boolean {
56 + let node = row;
57 + while (node !== null) {
58 + if (node.tag === SuspenseComponent) {
59 + const state: SuspenseState | null = node.memoizedState;
60 + if (state !== null) {
61 + return true;
62 + }
63 + } else if (node.child !== null) {
64 + node.child.return = node;
65 + node = node.child;
66 + continue;
67 + }
68 + if (node === row) {
69 + return false;
70 + }
71 + while (node.sibling === null) {
72 + if (node.return === null || node.return === row) {
73 + return false;
74 + }
75 + node = node.return;
76 + }
77 + node.sibling.return = node.return;
78 + node = node.sibling;
79 + }
80 + return false;
81 +}
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js
+466 -1
@@ -56,7 +56,46 @@ describe('ReactSuspenseList', () => {
56
57 expect(() => Scheduler.flushAll()).toWarnDev([
58 'Warning: "something" is not a supported revealOrder on ' +
59 - '<SuspenseList />. Did you mean "together"?' +
59 + '<SuspenseList />. Did you mean "together", "forwards" or "backwards"?' +
60 + '\n in SuspenseList (at **)' +
61 + '\n in Foo (at **)',
62 + ]);
63 + });
64 +
65 + it('warns if a upper case revealOrder option is used', () => {
66 + function Foo() {
67 + return (
68 + <SuspenseList revealOrder="TOGETHER">
69 + <Suspense fallback="Loading">Content</Suspense>
70 + </SuspenseList>
71 + );
72 + }
73 +
74 + ReactNoop.render(<Foo />);
75 +
76 + expect(() => Scheduler.flushAll()).toWarnDev([
77 + 'Warning: "TOGETHER" is not a valid value for revealOrder on ' +
78 + '<SuspenseList />. Use lowercase "together" instead.' +
79 + '\n in SuspenseList (at **)' +
80 + '\n in Foo (at **)',
81 + ]);
82 + });
83 +
84 + it('warns if a misspelled revealOrder option is used', () => {
85 + function Foo() {
86 + return (
87 + <SuspenseList revealOrder="forward">
88 + <Suspense fallback="Loading">Content</Suspense>
89 + </SuspenseList>
90 + );
91 + }
92 +
93 + ReactNoop.render(<Foo />);
94 +
95 + expect(() => Scheduler.flushAll()).toWarnDev([
96 + 'Warning: "forward" is not a valid value for revealOrder on ' +
97 + '<SuspenseList />. React uses the -s suffix in the spelling. ' +
98 + 'Use "forwards" instead.' +
99 '\n in SuspenseList (at **)' +
100 '\n in Foo (at **)',
101 ]);
@@ -562,4 +601,430 @@ describe('ReactSuspenseList', () => {
601 </Fragment>,
602 );
603 });
604 +
605 + it('displays each items in "forwards" order', async () => {
606 + let A = createAsyncText('A');
607 + let B = createAsyncText('B');
608 + let C = createAsyncText('C');
609 +
610 + function Foo() {
611 + return (
612 + <SuspenseList revealOrder="forwards">
613 + <Suspense fallback={<Text text="Loading A" />}>
614 + <A />
615 + </Suspense>
616 + <Suspense fallback={<Text text="Loading B" />}>
617 + <B />
618 + </Suspense>
619 + <Suspense fallback={<Text text="Loading C" />}>
620 + <C />
621 + </Suspense>
622 + </SuspenseList>
623 + );
624 + }
625 +
626 + await C.resolve();
627 +
628 + ReactNoop.render(<Foo />);
629 +
630 + expect(Scheduler).toFlushAndYield([
631 + 'Suspend! [A]',
632 + 'Loading A',
633 + 'Loading B',
634 + 'Loading C',
635 + ]);
636 +
637 + expect(ReactNoop).toMatchRenderedOutput(
638 + <Fragment>
639 + <span>Loading A</span>
640 + <span>Loading B</span>
641 + <span>Loading C</span>
642 + </Fragment>,
643 + );
644 +
645 + await A.resolve();
646 +
647 + expect(Scheduler).toFlushAndYield(['A', 'Suspend! [B]']);
648 +
649 + expect(ReactNoop).toMatchRenderedOutput(
650 + <Fragment>
651 + <span>A</span>
652 + <span>Loading B</span>
653 + <span>Loading C</span>
654 + </Fragment>,
655 + );
656 +
657 + await B.resolve();
658 +
659 + expect(Scheduler).toFlushAndYield(['B', 'C']);
660 +
661 + expect(ReactNoop).toMatchRenderedOutput(
662 + <Fragment>
663 + <span>A</span>
664 + <span>B</span>
665 + <span>C</span>
666 + </Fragment>,
667 + );
668 + });
669 +
670 + it('displays each items in "backwards" order', async () => {
671 + let A = createAsyncText('A');
672 + let B = createAsyncText('B');
673 + let C = createAsyncText('C');
674 +
675 + function Foo() {
676 + return (
677 + <SuspenseList revealOrder="backwards">
678 + <Suspense fallback={<Text text="Loading A" />}>
679 + <A />
680 + </Suspense>
681 + <Suspense fallback={<Text text="Loading B" />}>
682 + <B />
683 + </Suspense>
684 + <Suspense fallback={<Text text="Loading C" />}>
685 + <C />
686 + </Suspense>
687 + </SuspenseList>
688 + );
689 + }
690 +
691 + await A.resolve();
692 +
693 + ReactNoop.render(<Foo />);
694 +
695 + expect(Scheduler).toFlushAndYield([
696 + 'Suspend! [C]',
697 + 'Loading C',
698 + 'Loading B',
699 + 'Loading A',
700 + ]);
701 +
702 + expect(ReactNoop).toMatchRenderedOutput(
703 + <Fragment>
704 + <span>Loading A</span>
705 + <span>Loading B</span>
706 + <span>Loading C</span>
707 + </Fragment>,
708 + );
709 +
710 + await C.resolve();
711 +
712 + expect(Scheduler).toFlushAndYield(['C', 'Suspend! [B]']);
713 +
714 + expect(ReactNoop).toMatchRenderedOutput(
715 + <Fragment>
716 + <span>Loading A</span>
717 + <span>Loading B</span>
718 + <span>C</span>
719 + </Fragment>,
720 + );
721 +
722 + await B.resolve();
723 +
724 + expect(Scheduler).toFlushAndYield(['B', 'A']);
725 +
726 + expect(ReactNoop).toMatchRenderedOutput(
727 + <Fragment>
728 + <span>A</span>
729 + <span>B</span>
730 + <span>C</span>
731 + </Fragment>,
732 + );
733 + });
734 +
735 + it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
736 + let A = createAsyncText('A');
737 + let B = createAsyncText('B');
738 + let C = createAsyncText('C');
739 + let D = createAsyncText('D');
740 + let E = createAsyncText('E');
741 + let F = createAsyncText('F');
742 +
743 + function Foo({items}) {
744 + return (
745 + <SuspenseList revealOrder="forwards">
746 + {items.map(([key, Component]) => (
747 + <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
748 + <Component />
749 + </Suspense>
750 + ))}
751 + </SuspenseList>
752 + );
753 + }
754 +
755 + await B.resolve();
756 + await D.resolve();
757 +
758 + ReactNoop.render(<Foo items={[['B', B], ['D', D]]} />);
759 +
760 + expect(Scheduler).toFlushAndYield(['B', 'D']);
761 +
762 + expect(ReactNoop).toMatchRenderedOutput(
763 + <Fragment>
764 + <span>B</span>
765 + <span>D</span>
766 + </Fragment>,
767 + );
768 +
769 + // Insert items in the beginning, middle and end.
770 + ReactNoop.render(
771 + <Foo
772 + items={[['A', A], ['B', B], ['C', C], ['D', D], ['E', E], ['F', F]]}
773 + />,
774 + );
775 +
776 + expect(Scheduler).toFlushAndYield([
777 + 'Suspend! [A]',
778 + 'Loading A',
779 + 'B',
780 + 'Suspend! [C]',
781 + 'Loading C',
782 + 'D',
783 + 'Suspend! [E]',
784 + 'Loading E',
785 + 'Loading F',
786 + ]);
787 +
788 + expect(ReactNoop).toMatchRenderedOutput(
789 + <Fragment>
790 + <span>Loading A</span>
791 + <span>B</span>
792 + <span>Loading C</span>
793 + <span>D</span>
794 + <span>Loading E</span>
795 + <span>Loading F</span>
796 + </Fragment>,
797 + );
798 +
799 + await A.resolve();
800 +
801 + expect(Scheduler).toFlushAndYield(['A', 'Suspend! [C]']);
802 +
803 + // Even though we could show A, it is still in a fallback state because
804 + // C is not yet resolved. We need to resolve everything in the head first.
805 + expect(ReactNoop).toMatchRenderedOutput(
806 + <Fragment>
807 + <span>Loading A</span>
808 + <span>B</span>
809 + <span>Loading C</span>
810 + <span>D</span>
811 + <span>Loading E</span>
812 + <span>Loading F</span>
813 + </Fragment>,
814 + );
815 +
816 + await C.resolve();
817 +
818 + expect(Scheduler).toFlushAndYield(['A', 'C', 'Suspend! [E]']);
819 +
820 + // We can now resolve the full head.
821 + expect(ReactNoop).toMatchRenderedOutput(
822 + <Fragment>
823 + <span>A</span>
824 + <span>B</span>
825 + <span>C</span>
826 + <span>D</span>
827 + <span>Loading E</span>
828 + <span>Loading F</span>
829 + </Fragment>,
830 + );
831 +
832 + await E.resolve();
833 +
834 + expect(Scheduler).toFlushAndYield(['E', 'Suspend! [F]']);
835 +
836 + // In the tail we can resolve one-by-one.
837 + expect(ReactNoop).toMatchRenderedOutput(
838 + <Fragment>
839 + <span>A</span>
840 + <span>B</span>
841 + <span>C</span>
842 + <span>D</span>
843 + <span>E</span>
844 + <span>Loading F</span>
845 + </Fragment>,
846 + );
847 +
848 + await F.resolve();
849 +
850 + // We can also delete some items.
851 + ReactNoop.render(<Foo items={[['D', D], ['E', E], ['F', F]]} />);
852 +
853 + expect(Scheduler).toFlushAndYield(['D', 'E', 'F']);
854 +
855 + expect(ReactNoop).toMatchRenderedOutput(
856 + <Fragment>
857 + <span>D</span>
858 + <span>E</span>
859 + <span>F</span>
860 + </Fragment>,
861 + );
862 + });
863 +
864 + it('displays added row at the top "together" and the bottom in "forwards" order', async () => {
865 + let A = createAsyncText('A');
866 + let B = createAsyncText('B');
867 + let D = createAsyncText('D');
868 + let F = createAsyncText('F');
869 +
870 + function createSyncText(text) {
871 + return function() {
872 + return <Text text={text} />;
873 + };
874 + }
875 +
876 + let As = createSyncText('A');
877 + let Bs = createSyncText('B');
878 + let Cs = createSyncText('C');
879 + let Ds = createSyncText('D');
880 + let Es = createSyncText('E');
881 + let Fs = createSyncText('F');
882 +
883 + function Foo({items}) {
884 + return (
885 + <SuspenseList revealOrder="backwards">
886 + {items.map(([key, Component]) => (
887 + <Suspense key={key} fallback={<Text text={'Loading ' + key} />}>
888 + <Component />
889 + </Suspense>
890 + ))}
891 + </SuspenseList>
892 + );
893 + }
894 +
895 + // The first pass doesn't suspend.
896 + ReactNoop.render(
897 + <Foo
898 + items={[
899 + ['A', As],
900 + ['B', Bs],
901 + ['C', Cs],
902 + ['D', Ds],
903 + ['E', Es],
904 + ['F', Fs],
905 + ]}
906 + />,
907 + );
908 + expect(Scheduler).toFlushAndYield(['F', 'E', 'D', 'C', 'B', 'A']);
909 + expect(ReactNoop).toMatchRenderedOutput(
910 + <Fragment>
911 + <span>A</span>
912 + <span>B</span>
913 + <span>C</span>
914 + <span>D</span>
915 + <span>E</span>
916 + <span>F</span>
917 + </Fragment>,
918 + );
919 +
920 + // Update items in the beginning, middle and end to start suspending.
921 + ReactNoop.render(
922 + <Foo
923 + items={[['A', A], ['B', B], ['C', Cs], ['D', D], ['E', Es], ['F', F]]}
924 + />,
925 + );
926 +
927 + expect(Scheduler).toFlushAndYield([
928 + 'Suspend! [A]',
929 + 'Loading A',
930 + 'Suspend! [B]',
931 + 'Loading B',
932 + 'C',
933 + 'Suspend! [D]',
934 + 'Loading D',
935 + 'E',
936 + 'Suspend! [F]',
937 + 'Loading F',
938 + ]);
939 +
940 + // This will suspend, since the boundaries are avoided. Give them
941 + // time to display their loading states.
942 + jest.advanceTimersByTime(500);
943 +
944 + expect(ReactNoop).toMatchRenderedOutput(
945 + <Fragment>
946 + <span hidden={true}>A</span>
947 + <span>Loading A</span>
948 + <span hidden={true}>B</span>
949 + <span>Loading B</span>
950 + <span>C</span>
951 + <span hidden={true}>D</span>
952 + <span>Loading D</span>
953 + <span>E</span>
954 + <span hidden={true}>F</span>
955 + <span>Loading F</span>
956 + </Fragment>,
957 + );
958 +
959 + await F.resolve();
960 +
961 + expect(Scheduler).toFlushAndYield(['F']);
962 +
963 + // Even though we could show F, it is still in a fallback state because
964 + // E is not yet resolved. We need to resolve everything in the head first.
965 + expect(ReactNoop).toMatchRenderedOutput(
966 + <Fragment>
967 + <span hidden={true}>A</span>
968 + <span>Loading A</span>
969 + <span hidden={true}>B</span>
970 + <span>Loading B</span>
971 + <span>C</span>
972 + <span hidden={true}>D</span>
973 + <span>Loading D</span>
974 + <span>E</span>
975 + <span hidden={true}>F</span>
976 + <span>Loading F</span>
977 + </Fragment>,
978 + );
979 +
980 + await D.resolve();
981 +
982 + expect(Scheduler).toFlushAndYield(['D', 'F', 'Suspend! [B]']);
983 +
984 + // We can now resolve the full head.
985 + expect(ReactNoop).toMatchRenderedOutput(
986 + <Fragment>
987 + <span hidden={true}>A</span>
988 + <span>Loading A</span>
989 + <span hidden={true}>B</span>
990 + <span>Loading B</span>
991 + <span>C</span>
992 + <span>D</span>
993 + <span>E</span>
994 + <span>F</span>
995 + </Fragment>,
996 + );
997 +
998 + await B.resolve();
999 +
1000 + expect(Scheduler).toFlushAndYield(['B', 'Suspend! [A]']);
1001 +
1002 + // In the tail we can resolve one-by-one.
1003 + expect(ReactNoop).toMatchRenderedOutput(
1004 + <Fragment>
1005 + <span hidden={true}>A</span>
1006 + <span>Loading A</span>
1007 + <span>B</span>
1008 + <span>C</span>
1009 + <span>D</span>
1010 + <span>E</span>
1011 + <span>F</span>
1012 + </Fragment>,
1013 + );
1014 +
1015 + await A.resolve();
1016 +
1017 + expect(Scheduler).toFlushAndYield(['A']);
1018 +
1019 + expect(ReactNoop).toMatchRenderedOutput(
1020 + <Fragment>
1021 + <span>A</span>
1022 + <span>B</span>
1023 + <span>C</span>
1024 + <span>D</span>
1025 + <span>E</span>
1026 + <span>F</span>
1027 + </Fragment>,
1028 + );
1029 + });
1030 });