@@ -15,6 +15,7 @@ let ReactDOMServer;
15
15
let Scheduler ;
16
16
let ReactFeatureFlags ;
17
17
let Suspense ;
18
+ let SuspenseList ;
18
19
let act ;
19
20
20
21
describe ( 'ReactDOMServerPartialHydration' , ( ) => {
@@ -30,6 +31,7 @@ describe('ReactDOMServerPartialHydration', () => {
30
31
ReactDOMServer = require ( 'react-dom/server' ) ;
31
32
Scheduler = require ( 'scheduler' ) ;
32
33
Suspense = React . Suspense ;
34
+ SuspenseList = React . unstable_SuspenseList ;
33
35
} ) ;
34
36
35
37
it ( 'hydrates a parent even if a child Suspense boundary is blocked' , async ( ) => {
@@ -1077,6 +1079,256 @@ describe('ReactDOMServerPartialHydration', () => {
1077
1079
expect ( ref . current ) . toBe ( div ) ;
1078
1080
} ) ;
1079
1081
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
+
1080
1332
it ( 'can client render nested boundaries' , async ( ) => {
1081
1333
let suspend = false ;
1082
1334
let promise = new Promise ( ( ) => { } ) ;
0 commit comments