@@ -16,7 +16,12 @@ let Scheduler;
16
16
let Suspense ;
17
17
let useState ;
18
18
let useTransition ;
19
+ let startTransition ;
19
20
let act ;
21
+ let getCacheForType ;
22
+
23
+ let caches ;
24
+ let seededCache ;
20
25
21
26
describe ( 'ReactTransition' , ( ) => {
22
27
beforeEach ( ( ) => {
@@ -27,44 +32,142 @@ describe('ReactTransition', () => {
27
32
useState = React . useState ;
28
33
useTransition = React . unstable_useTransition ;
29
34
Suspense = React . Suspense ;
35
+ startTransition = React . unstable_startTransition ;
36
+ getCacheForType = React . unstable_getCacheForType ;
30
37
act = ReactNoop . act ;
38
+
39
+ caches = [ ] ;
40
+ seededCache = null ;
31
41
} ) ;
32
42
33
- function Text ( props ) {
34
- Scheduler . unstable_yieldValue ( props . text ) ;
35
- return props . text ;
43
+ function createTextCache ( ) {
44
+ if ( seededCache !== null ) {
45
+ // Trick to seed a cache before it exists.
46
+ // TODO: Need a built-in API to seed data before the initial render (i.e.
47
+ // not a refresh because nothing has mounted yet).
48
+ const cache = seededCache ;
49
+ seededCache = null ;
50
+ return cache ;
51
+ }
52
+
53
+ const data = new Map ( ) ;
54
+ const version = caches . length + 1 ;
55
+ const cache = {
56
+ version,
57
+ data,
58
+ resolve ( text ) {
59
+ const record = data . get ( text ) ;
60
+ if ( record === undefined ) {
61
+ const newRecord = {
62
+ status : 'resolved' ,
63
+ value : text ,
64
+ } ;
65
+ data . set ( text , newRecord ) ;
66
+ } else if ( record . status === 'pending' ) {
67
+ const thenable = record . value ;
68
+ record . status = 'resolved' ;
69
+ record . value = text ;
70
+ thenable . pings . forEach ( t => t ( ) ) ;
71
+ }
72
+ } ,
73
+ reject ( text , error ) {
74
+ const record = data . get ( text ) ;
75
+ if ( record === undefined ) {
76
+ const newRecord = {
77
+ status : 'rejected' ,
78
+ value : error ,
79
+ } ;
80
+ data . set ( text , newRecord ) ;
81
+ } else if ( record . status === 'pending' ) {
82
+ const thenable = record . value ;
83
+ record . status = 'rejected' ;
84
+ record . value = error ;
85
+ thenable . pings . forEach ( t => t ( ) ) ;
86
+ }
87
+ } ,
88
+ } ;
89
+ caches . push ( cache ) ;
90
+ return cache ;
36
91
}
37
92
38
- function createAsyncText ( text ) {
39
- let resolved = false ;
40
- const Component = function ( ) {
41
- if ( ! resolved ) {
42
- Scheduler . unstable_yieldValue ( 'Suspend! [' + text + ']' ) ;
43
- throw promise ;
93
+ function readText ( text ) {
94
+ const textCache = getCacheForType ( createTextCache ) ;
95
+ const record = textCache . data . get ( text ) ;
96
+ if ( record !== undefined ) {
97
+ switch ( record . status ) {
98
+ case 'pending' :
99
+ Scheduler . unstable_yieldValue ( `Suspend! [${ text } ]` ) ;
100
+ throw record . value ;
101
+ case 'rejected' :
102
+ Scheduler . unstable_yieldValue ( `Error! [${ text } ]` ) ;
103
+ throw record . value ;
104
+ case 'resolved' :
105
+ return textCache . version ;
44
106
}
45
- return < Text text = { text } /> ;
46
- } ;
47
- const promise = new Promise ( resolve => {
48
- Component . resolve = function ( ) {
49
- resolved = true ;
50
- return resolve ( ) ;
107
+ } else {
108
+ Scheduler . unstable_yieldValue ( `Suspend! [${ text } ]` ) ;
109
+
110
+ const thenable = {
111
+ pings : [ ] ,
112
+ then ( resolve ) {
113
+ if ( newRecord . status === 'pending' ) {
114
+ thenable . pings . push ( resolve ) ;
115
+ } else {
116
+ Promise . resolve ( ) . then ( ( ) => resolve ( newRecord . value ) ) ;
117
+ }
118
+ } ,
51
119
} ;
52
- } ) ;
53
- return Component ;
120
+
121
+ const newRecord = {
122
+ status : 'pending' ,
123
+ value : thenable ,
124
+ } ;
125
+ textCache . data . set ( text , newRecord ) ;
126
+
127
+ throw thenable ;
128
+ }
129
+ }
130
+
131
+ function Text ( { text} ) {
132
+ Scheduler . unstable_yieldValue ( text ) ;
133
+ return text ;
134
+ }
135
+
136
+ function AsyncText ( { text} ) {
137
+ readText ( text ) ;
138
+ Scheduler . unstable_yieldValue ( text ) ;
139
+ return text ;
140
+ }
141
+
142
+ function seedNextTextCache ( text ) {
143
+ if ( seededCache === null ) {
144
+ seededCache = createTextCache ( ) ;
145
+ }
146
+ seededCache . resolve ( text ) ;
147
+ }
148
+
149
+ function resolveText ( text ) {
150
+ if ( caches . length === 0 ) {
151
+ throw Error ( 'Cache does not exist.' ) ;
152
+ } else {
153
+ // Resolve the most recently created cache. An older cache can by
154
+ // resolved with `caches[index].resolve(text)`.
155
+ caches [ caches . length - 1 ] . resolve ( text ) ;
156
+ }
54
157
}
55
158
56
159
// @gate experimental
57
- it ( 'isPending works even if called from outside an input event' , async ( ) => {
58
- const Async = createAsyncText ( 'Async' ) ;
160
+ // @gate enableCache
161
+ test ( 'isPending works even if called from outside an input event' , async ( ) => {
59
162
let start ;
60
163
function App ( ) {
61
164
const [ show , setShow ] = useState ( false ) ;
62
- const [ startTransition , isPending ] = useTransition ( ) ;
63
- start = ( ) => startTransition ( ( ) => setShow ( true ) ) ;
165
+ const [ _start , isPending ] = useTransition ( ) ;
166
+ start = ( ) => _start ( ( ) => setShow ( true ) ) ;
64
167
return (
65
168
< Suspense fallback = { < Text text = "Loading..." /> } >
66
169
{ isPending ? < Text text = "Pending..." /> : null }
67
- { show ? < Async /> : < Text text = "(empty)" /> }
170
+ { show ? < AsyncText text = " Async" /> : < Text text = "(empty)" /> }
68
171
</ Suspense >
69
172
) ;
70
173
}
@@ -89,9 +192,254 @@ describe('ReactTransition', () => {
89
192
90
193
expect ( root ) . toMatchRenderedOutput ( 'Pending...(empty)' ) ;
91
194
92
- await Async . resolve ( ) ;
195
+ await resolveText ( 'Async' ) ;
93
196
} ) ;
94
197
expect ( Scheduler ) . toHaveYielded ( [ 'Async' ] ) ;
95
198
expect ( root ) . toMatchRenderedOutput ( 'Async' ) ;
96
199
} ) ;
200
+
201
+ // @gate experimental
202
+ // @gate enableCache
203
+ test (
204
+ 'when multiple transitions update the same queue, only the most recent ' +
205
+ 'one is allowed to finish (no intermediate states)' ,
206
+ async ( ) => {
207
+ let update ;
208
+ function App ( ) {
209
+ const [ startContentChange , isContentPending ] = useTransition ( ) ;
210
+ const [ label , setLabel ] = useState ( 'A' ) ;
211
+ const [ contents , setContents ] = useState ( 'A' ) ;
212
+ update = value => {
213
+ ReactNoop . discreteUpdates ( ( ) => {
214
+ setLabel ( value ) ;
215
+ startContentChange ( ( ) => {
216
+ setContents ( value ) ;
217
+ } ) ;
218
+ } ) ;
219
+ } ;
220
+ return (
221
+ < >
222
+ < Text
223
+ text = {
224
+ label + ' label' + ( isContentPending ? ' (loading...)' : '' )
225
+ }
226
+ />
227
+ < div >
228
+ < Suspense fallback = { < Text text = "Loading..." /> } >
229
+ < AsyncText text = { contents + ' content' } />
230
+ </ Suspense >
231
+ </ div >
232
+ </ >
233
+ ) ;
234
+ }
235
+
236
+ // Initial render
237
+ const root = ReactNoop . createRoot ( ) ;
238
+ await act ( async ( ) => {
239
+ seedNextTextCache ( 'A content' ) ;
240
+ root . render ( < App /> ) ;
241
+ } ) ;
242
+ expect ( Scheduler ) . toHaveYielded ( [ 'A label' , 'A content' ] ) ;
243
+ expect ( root ) . toMatchRenderedOutput (
244
+ < >
245
+ A label< div > A content</ div >
246
+ </ > ,
247
+ ) ;
248
+
249
+ // Switch to B
250
+ await act ( async ( ) => {
251
+ update ( 'B' ) ;
252
+ } ) ;
253
+ expect ( Scheduler ) . toHaveYielded ( [
254
+ // Commit pending state
255
+ 'B label (loading...)' ,
256
+ 'A content' ,
257
+
258
+ // Attempt to render B, but it suspends
259
+ 'B label' ,
260
+ 'Suspend! [B content]' ,
261
+ 'Loading...' ,
262
+ ] ) ;
263
+ // This is a refresh transition so it shouldn't show a fallback
264
+ expect ( root ) . toMatchRenderedOutput (
265
+ < >
266
+ B label (loading...)< div > A content</ div >
267
+ </ > ,
268
+ ) ;
269
+
270
+ // Before B finishes loading, switch to C
271
+ await act ( async ( ) => {
272
+ update ( 'C' ) ;
273
+ } ) ;
274
+ expect ( Scheduler ) . toHaveYielded ( [
275
+ // Commit pending state
276
+ 'C label (loading...)' ,
277
+ 'A content' ,
278
+
279
+ // Attempt to render C, but it suspends
280
+ 'C label' ,
281
+ 'Suspend! [C content]' ,
282
+ 'Loading...' ,
283
+ ] ) ;
284
+ expect ( root ) . toMatchRenderedOutput (
285
+ < >
286
+ C label (loading...)< div > A content</ div >
287
+ </ > ,
288
+ ) ;
289
+
290
+ // Finish loading B. But we're not allowed to render B because it's
291
+ // entangled with C. So we're still pending.
292
+ await act ( async ( ) => {
293
+ resolveText ( 'B content' ) ;
294
+ } ) ;
295
+ expect ( Scheduler ) . toHaveYielded ( [
296
+ // Attempt to render C, but it suspends
297
+ 'C label' ,
298
+ 'Suspend! [C content]' ,
299
+ 'Loading...' ,
300
+ ] ) ;
301
+ expect ( root ) . toMatchRenderedOutput (
302
+ < >
303
+ C label (loading...)< div > A content</ div >
304
+ </ > ,
305
+ ) ;
306
+
307
+ // Now finish loading C. This is the terminal update, so it can finish.
308
+ await act ( async ( ) => {
309
+ resolveText ( 'C content' ) ;
310
+ } ) ;
311
+ expect ( Scheduler ) . toHaveYielded ( [ 'C label' , 'C content' ] ) ;
312
+ expect ( root ) . toMatchRenderedOutput (
313
+ < >
314
+ C label< div > C content</ div >
315
+ </ > ,
316
+ ) ;
317
+ } ,
318
+ ) ;
319
+
320
+ // Same as previous test, but for class update queue.
321
+ // @gate experimental
322
+ // @gate enableCache
323
+ test (
324
+ 'when multiple transitions update the same queue, only the most recent ' +
325
+ 'one is allowed to finish (no intermediate states) (classes)' ,
326
+ async ( ) => {
327
+ let update ;
328
+ class App extends React . Component {
329
+ state = {
330
+ label : 'A' ,
331
+ contents : 'A' ,
332
+ } ;
333
+ render ( ) {
334
+ update = value => {
335
+ ReactNoop . discreteUpdates ( ( ) => {
336
+ this . setState ( { label : value } ) ;
337
+ startTransition ( ( ) => {
338
+ this . setState ( { contents : value } ) ;
339
+ } ) ;
340
+ } ) ;
341
+ } ;
342
+ const label = this . state . label ;
343
+ const contents = this . state . contents ;
344
+ const isContentPending = label !== contents ;
345
+ return (
346
+ < >
347
+ < Text
348
+ text = {
349
+ label + ' label' + ( isContentPending ? ' (loading...)' : '' )
350
+ }
351
+ />
352
+ < div >
353
+ < Suspense fallback = { < Text text = "Loading..." /> } >
354
+ < AsyncText text = { contents + ' content' } />
355
+ </ Suspense >
356
+ </ div >
357
+ </ >
358
+ ) ;
359
+ }
360
+ }
361
+
362
+ // Initial render
363
+ const root = ReactNoop . createRoot ( ) ;
364
+ await act ( async ( ) => {
365
+ seedNextTextCache ( 'A content' ) ;
366
+ root . render ( < App /> ) ;
367
+ } ) ;
368
+ expect ( Scheduler ) . toHaveYielded ( [ 'A label' , 'A content' ] ) ;
369
+ expect ( root ) . toMatchRenderedOutput (
370
+ < >
371
+ A label< div > A content</ div >
372
+ </ > ,
373
+ ) ;
374
+
375
+ // Switch to B
376
+ await act ( async ( ) => {
377
+ update ( 'B' ) ;
378
+ } ) ;
379
+ expect ( Scheduler ) . toHaveYielded ( [
380
+ // Commit pending state
381
+ 'B label (loading...)' ,
382
+ 'A content' ,
383
+
384
+ // Attempt to render B, but it suspends
385
+ 'B label' ,
386
+ 'Suspend! [B content]' ,
387
+ 'Loading...' ,
388
+ ] ) ;
389
+ // This is a refresh transition so it shouldn't show a fallback
390
+ expect ( root ) . toMatchRenderedOutput (
391
+ < >
392
+ B label (loading...)< div > A content</ div >
393
+ </ > ,
394
+ ) ;
395
+
396
+ // Before B finishes loading, switch to C
397
+ await act ( async ( ) => {
398
+ update ( 'C' ) ;
399
+ } ) ;
400
+ expect ( Scheduler ) . toHaveYielded ( [
401
+ // Commit pending state
402
+ 'C label (loading...)' ,
403
+ 'A content' ,
404
+
405
+ // Attempt to render C, but it suspends
406
+ 'C label' ,
407
+ 'Suspend! [C content]' ,
408
+ 'Loading...' ,
409
+ ] ) ;
410
+ expect ( root ) . toMatchRenderedOutput (
411
+ < >
412
+ C label (loading...)< div > A content</ div >
413
+ </ > ,
414
+ ) ;
415
+
416
+ // Finish loading B. But we're not allowed to render B because it's
417
+ // entangled with C. So we're still pending.
418
+ await act ( async ( ) => {
419
+ resolveText ( 'B content' ) ;
420
+ } ) ;
421
+ expect ( Scheduler ) . toHaveYielded ( [
422
+ // Attempt to render C, but it suspends
423
+ 'C label' ,
424
+ 'Suspend! [C content]' ,
425
+ 'Loading...' ,
426
+ ] ) ;
427
+ expect ( root ) . toMatchRenderedOutput (
428
+ < >
429
+ C label (loading...)< div > A content</ div >
430
+ </ > ,
431
+ ) ;
432
+
433
+ // Now finish loading C. This is the terminal update, so it can finish.
434
+ await act ( async ( ) => {
435
+ resolveText ( 'C content' ) ;
436
+ } ) ;
437
+ expect ( Scheduler ) . toHaveYielded ( [ 'C label' , 'C content' ] ) ;
438
+ expect ( root ) . toMatchRenderedOutput (
439
+ < >
440
+ C label< div > C content</ div >
441
+ </ > ,
442
+ ) ;
443
+ } ,
444
+ ) ;
97
445
} ) ;
0 commit comments