@@ -15,6 +15,7 @@ let ReactDOMServer;
1515let Scheduler ;
1616let ReactFeatureFlags ;
1717let Suspense ;
18+ let SuspenseList ;
1819let act ;
1920
2021describe ( 'ReactDOMServerPartialHydration' , ( ) => {
@@ -30,6 +31,7 @@ describe('ReactDOMServerPartialHydration', () => {
3031 ReactDOMServer = require ( 'react-dom/server' ) ;
3132 Scheduler = require ( 'scheduler' ) ;
3233 Suspense = React . Suspense ;
34+ SuspenseList = React . unstable_SuspenseList ;
3335 } ) ;
3436
3537 it ( 'hydrates a parent even if a child Suspense boundary is blocked' , async ( ) => {
@@ -1077,6 +1079,256 @@ describe('ReactDOMServerPartialHydration', () => {
10771079 expect ( ref . current ) . toBe ( div ) ;
10781080 } ) ;
10791081
1082+ it ( 'shows inserted items in a SuspenseList before content is hydrated' , async ( ) => {
1083+ let suspend = false ;
1084+ let resolve ;
1085+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
1086+ let ref = React . createRef ( ) ;
1087+
1088+ function Child ( { children} ) {
1089+ if ( suspend ) {
1090+ throw promise ;
1091+ } else {
1092+ return children ;
1093+ }
1094+ }
1095+
1096+ // These are hoisted to avoid them from rerendering.
1097+ const a = (
1098+ < Suspense fallback = "Loading A" >
1099+ < Child >
1100+ < span > A</ span >
1101+ </ Child >
1102+ </ Suspense >
1103+ ) ;
1104+ const b = (
1105+ < Suspense fallback = "Loading B" >
1106+ < Child >
1107+ < span ref = { ref } > B</ span >
1108+ </ Child >
1109+ </ Suspense >
1110+ ) ;
1111+
1112+ function App ( { showMore} ) {
1113+ return (
1114+ < SuspenseList revealOrder = "forwards" >
1115+ { a }
1116+ { b }
1117+ { showMore ? (
1118+ < Suspense fallback = "Loading C" >
1119+ < span > C</ span >
1120+ </ Suspense >
1121+ ) : null }
1122+ </ SuspenseList >
1123+ ) ;
1124+ }
1125+
1126+ suspend = false ;
1127+ let html = ReactDOMServer . renderToString ( < App showMore = { false } /> ) ;
1128+
1129+ let container = document . createElement ( 'div' ) ;
1130+ container . innerHTML = html ;
1131+
1132+ let spanB = container . getElementsByTagName ( 'span' ) [ 1 ] ;
1133+
1134+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
1135+
1136+ suspend = true ;
1137+ act ( ( ) => {
1138+ root . render ( < App showMore = { false } /> ) ;
1139+ } ) ;
1140+
1141+ // We're not hydrated yet.
1142+ expect ( ref . current ) . toBe ( null ) ;
1143+ expect ( container . textContent ) . toBe ( 'AB' ) ;
1144+
1145+ // Add more rows before we've hydrated the first two.
1146+ act ( ( ) => {
1147+ root . render ( < App showMore = { true } /> ) ;
1148+ } ) ;
1149+
1150+ // We're not hydrated yet.
1151+ expect ( ref . current ) . toBe ( null ) ;
1152+
1153+ // Since the first two are already showing their final content
1154+ // we should be able to show the real content.
1155+ expect ( container . textContent ) . toBe ( 'ABC' ) ;
1156+
1157+ suspend = false ;
1158+ await act ( async ( ) => {
1159+ await resolve ( ) ;
1160+ } ) ;
1161+
1162+ expect ( container . textContent ) . toBe ( 'ABC' ) ;
1163+ // We've hydrated the same span.
1164+ expect ( ref . current ) . toBe ( spanB ) ;
1165+ } ) ;
1166+
1167+ it ( 'shows is able to hydrate boundaries even if others in a list are pending' , async ( ) => {
1168+ let suspend = false ;
1169+ let resolve ;
1170+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
1171+ let ref = React . createRef ( ) ;
1172+
1173+ function Child ( { children} ) {
1174+ if ( suspend ) {
1175+ throw promise ;
1176+ } else {
1177+ return children ;
1178+ }
1179+ }
1180+
1181+ let promise2 = new Promise ( ( ) => { } ) ;
1182+ function AlwaysSuspend ( ) {
1183+ throw promise2 ;
1184+ }
1185+
1186+ // This is hoisted to avoid them from rerendering.
1187+ const a = (
1188+ < Suspense fallback = "Loading A" >
1189+ < Child >
1190+ < span ref = { ref } > A</ span >
1191+ </ Child >
1192+ </ Suspense >
1193+ ) ;
1194+
1195+ function App ( { showMore} ) {
1196+ return (
1197+ < SuspenseList revealOrder = "together" >
1198+ { a }
1199+ { showMore ? (
1200+ < Suspense fallback = "Loading B" >
1201+ < AlwaysSuspend />
1202+ </ Suspense >
1203+ ) : null }
1204+ </ SuspenseList >
1205+ ) ;
1206+ }
1207+
1208+ suspend = false ;
1209+ let html = ReactDOMServer . renderToString ( < App showMore = { false } /> ) ;
1210+
1211+ let container = document . createElement ( 'div' ) ;
1212+ container . innerHTML = html ;
1213+
1214+ let spanA = container . getElementsByTagName ( 'span' ) [ 0 ] ;
1215+
1216+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
1217+
1218+ suspend = true ;
1219+ act ( ( ) => {
1220+ root . render ( < App showMore = { false } /> ) ;
1221+ } ) ;
1222+
1223+ // We're not hydrated yet.
1224+ expect ( ref . current ) . toBe ( null ) ;
1225+ expect ( container . textContent ) . toBe ( 'A' ) ;
1226+
1227+ await act ( async ( ) => {
1228+ // Add another row before we've hydrated the first one.
1229+ root . render ( < App showMore = { true } /> ) ;
1230+ // At the same time, we resolve the blocking promise.
1231+ suspend = false ;
1232+ await resolve ( ) ;
1233+ } ) ;
1234+
1235+ // We should have been able to hydrate the first row.
1236+ expect ( ref . current ) . toBe ( spanA ) ;
1237+ // Even though we're still slowing B.
1238+ expect ( container . textContent ) . toBe ( 'ALoading B' ) ;
1239+ } ) ;
1240+
1241+ it ( 'shows inserted items before pending in a SuspenseList as fallbacks' , async ( ) => {
1242+ let suspend = false ;
1243+ let resolve ;
1244+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
1245+ let ref = React . createRef ( ) ;
1246+
1247+ function Child ( { children} ) {
1248+ if ( suspend ) {
1249+ throw promise ;
1250+ } else {
1251+ return children ;
1252+ }
1253+ }
1254+
1255+ // These are hoisted to avoid them from rerendering.
1256+ const a = (
1257+ < Suspense fallback = "Loading A" >
1258+ < Child >
1259+ < span > A</ span >
1260+ </ Child >
1261+ </ Suspense >
1262+ ) ;
1263+ const b = (
1264+ < Suspense fallback = "Loading B" >
1265+ < Child >
1266+ < span ref = { ref } > B</ span >
1267+ </ Child >
1268+ </ Suspense >
1269+ ) ;
1270+
1271+ function App ( { showMore} ) {
1272+ return (
1273+ < SuspenseList revealOrder = "forwards" >
1274+ { a }
1275+ { b }
1276+ { showMore ? (
1277+ < Suspense fallback = "Loading C" >
1278+ < span > C</ span >
1279+ </ Suspense >
1280+ ) : null }
1281+ </ SuspenseList >
1282+ ) ;
1283+ }
1284+
1285+ suspend = false ;
1286+ let html = ReactDOMServer . renderToString ( < App showMore = { false } /> ) ;
1287+
1288+ let container = document . createElement ( 'div' ) ;
1289+ container . innerHTML = html ;
1290+
1291+ let suspenseNode = container . firstChild ;
1292+ expect ( suspenseNode . nodeType ) . toBe ( 8 ) ;
1293+ // Put the suspense node in pending state.
1294+ suspenseNode . data = '$?' ;
1295+
1296+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
1297+
1298+ suspend = true ;
1299+ act ( ( ) => {
1300+ root . render ( < App showMore = { false } /> ) ;
1301+ } ) ;
1302+
1303+ // We're not hydrated yet.
1304+ expect ( ref . current ) . toBe ( null ) ;
1305+ expect ( container . textContent ) . toBe ( 'AB' ) ;
1306+
1307+ // Add more rows before we've hydrated the first two.
1308+ act ( ( ) => {
1309+ root . render ( < App showMore = { true } /> ) ;
1310+ } ) ;
1311+
1312+ // We're not hydrated yet.
1313+ expect ( ref . current ) . toBe ( null ) ;
1314+
1315+ // Since the first two are already showing their final content
1316+ // we should be able to show the real content.
1317+ expect ( container . textContent ) . toBe ( 'ABLoading C' ) ;
1318+
1319+ suspend = false ;
1320+ await act ( async ( ) => {
1321+ // Resolve the boundary to be in its resolved final state.
1322+ suspenseNode . data = '$' ;
1323+ if ( suspenseNode . _reactRetry ) {
1324+ suspenseNode . _reactRetry ( ) ;
1325+ }
1326+ await resolve ( ) ;
1327+ } ) ;
1328+
1329+ expect ( container . textContent ) . toBe ( 'ABC' ) ;
1330+ } ) ;
1331+
10801332 it ( 'can client render nested boundaries' , async ( ) => {
10811333 let suspend = false ;
10821334 let promise = new Promise ( ( ) => { } ) ;
0 commit comments