1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+'use strict';
12
+
13
let React;
2
-let ReactNoop;
3
-let Cache;
4
-let getCacheSignal;
5
-let Scheduler;
6
-let assertLog;
7
-let act;
8
-let Suspense;
9
-let Activity;
10
-let useCacheRefresh;
11
-let startTransition;
12
-let useState;
14
+let ReactNoopFlightServer;
15
+let ReactNoopFlightClient;
16
let cache;
17
15
-let getTextCache;
16
-let textCaches;
17
-let seededCache;
18
-
18
describe('ReactCache', () => {
19
beforeEach(() => {
20
jest.resetModules();
22
-
21
+ jest.mock('react', () => require('react/react.react-server'));
22
React = require('react');
24
- ReactNoop = require('react-noop-renderer');
25
- Cache = React.unstable_Cache;
26
- Scheduler = require('scheduler');
27
- act = require('internal-test-utils').act;
28
- Suspense = React.Suspense;
29
- cache = React.cache;
30
- Activity = React.unstable_Activity;
31
- getCacheSignal = React.unstable_getCacheSignal;
32
- useCacheRefresh = React.unstable_useCacheRefresh;
33
- startTransition = React.startTransition;
34
- useState = React.useState;
35
-
36
- const InternalTestUtils = require('internal-test-utils');
37
- assertLog = InternalTestUtils.assertLog;
38
-
39
- textCaches = [];
40
- seededCache = null;
41
-
42
- if (gate(flags => flags.enableCache)) {
43
- getTextCache = cache(() => {
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 textCache = seededCache;
49
- seededCache = null;
50
- return textCache;
51
- }
52
-
53
- const data = new Map();
54
- const version = textCaches.length + 1;
55
- const textCache = {
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
- cleanupScheduled: false,
65
- };
66
- data.set(text, newRecord);
67
- } else if (record.status === 'pending') {
68
- record.value.resolve();
69
- }
70
- },
71
- reject(text, error) {
72
- const record = data.get(text);
73
- if (record === undefined) {
74
- const newRecord = {
75
- status: 'rejected',
76
- value: error,
77
- cleanupScheduled: false,
78
- };
79
- data.set(text, newRecord);
80
- } else if (record.status === 'pending') {
81
- record.value.reject();
82
- }
83
- },
84
- };
85
- textCaches.push(textCache);
86
- return textCache;
87
- });
88
- }
89
- });
90
-
91
- function readText(text) {
92
- const signal = getCacheSignal ? getCacheSignal() : null;
93
- const textCache = getTextCache();
94
- const record = textCache.data.get(text);
95
- if (record !== undefined) {
96
- if (!record.cleanupScheduled) {
97
- // This record was seeded prior to the abort signal being available:
98
- // schedule a cleanup function for it.
99
- // TODO: Add ability to cleanup entries seeded w useCacheRefresh()
100
- record.cleanupScheduled = true;
101
- if (getCacheSignal) {
102
- signal.addEventListener('abort', () => {
103
- Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`);
104
- });
105
- }
106
- }
107
- switch (record.status) {
108
- case 'pending':
109
- throw record.value;
110
- case 'rejected':
111
- throw record.value;
112
- case 'resolved':
113
- return textCache.version;
114
- }
115
- } else {
116
- Scheduler.log(`Cache miss! [${text}]`);
117
-
118
- let resolve;
119
- let reject;
120
- const thenable = new Promise((res, rej) => {
121
- resolve = res;
122
- reject = rej;
123
- }).then(
124
- value => {
125
- if (newRecord.status === 'pending') {
126
- newRecord.status = 'resolved';
127
- newRecord.value = value;
128
- }
129
- },
130
- error => {
131
- if (newRecord.status === 'pending') {
132
- newRecord.status = 'rejected';
133
- newRecord.value = error;
134
- }
135
- },
136
- );
137
- thenable.resolve = resolve;
138
- thenable.reject = reject;
139
-
140
- const newRecord = {
141
- status: 'pending',
142
- value: thenable,
143
- cleanupScheduled: true,
144
- };
145
- textCache.data.set(text, newRecord);
146
-
147
- if (getCacheSignal) {
148
- signal.addEventListener('abort', () => {
149
- Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`);
150
- });
151
- }
152
- throw thenable;
153
- }
154
- }
155
-
156
- function Text({text}) {
157
- Scheduler.log(text);
158
- return text;
159
- }
160
-
161
- function AsyncText({text, showVersion}) {
162
- const version = readText(text);
163
- const fullText = showVersion ? `${text} [v${version}]` : text;
164
- Scheduler.log(fullText);
165
- return fullText;
166
- }
167
-
168
- function seedNextTextCache(text) {
169
- if (seededCache === null) {
170
- seededCache = getTextCache();
171
- }
172
- seededCache.resolve(text);
173
- }
174
-
175
- function resolveMostRecentTextCache(text) {
176
- if (textCaches.length === 0) {
177
- throw Error('Cache does not exist.');
178
- } else {
179
- // Resolve the most recently created cache. An older cache can by
180
- // resolved with `textCaches[index].resolve(text)`.
181
- textCaches[textCaches.length - 1].resolve(text);
182
- }
183
- }
184
-
185
- // @gate enableCacheElement && enableCache
186
- test('render Cache component', async () => {
187
- const root = ReactNoop.createRoot();
188
- await act(() => {
189
- root.render(<Cache>Hi</Cache>);
190
- });
191
- expect(root).toMatchRenderedOutput('Hi');
192
- });
193
-
194
- // @gate enableCacheElement && enableCache
195
- test('mount new data', async () => {
196
- const root = ReactNoop.createRoot();
197
- await act(() => {
198
- root.render(
199
- <Cache>
200
- <Suspense fallback={<Text text="Loading..." />}>
201
- <AsyncText text="A" />
202
- </Suspense>
203
- </Cache>,
204
- );
205
- });
206
- assertLog(['Cache miss! [A]', 'Loading...']);
207
- expect(root).toMatchRenderedOutput('Loading...');
208
-
209
- await act(() => {
210
- resolveMostRecentTextCache('A');
211
- });
212
- assertLog(['A']);
213
- expect(root).toMatchRenderedOutput('A');
214
-
215
- await act(() => {
216
- root.render('Bye');
217
- });
218
- // no cleanup: cache is still retained at the root
219
- assertLog([]);
220
- expect(root).toMatchRenderedOutput('Bye');
221
- });
222
-
223
- // @gate enableCache
224
- test('root acts as implicit cache boundary', async () => {
225
- const root = ReactNoop.createRoot();
226
- await act(() => {
227
- root.render(
228
- <Suspense fallback={<Text text="Loading..." />}>
229
- <AsyncText text="A" />
230
- </Suspense>,
231
- );
232
- });
233
- assertLog(['Cache miss! [A]', 'Loading...']);
234
- expect(root).toMatchRenderedOutput('Loading...');
235
-
236
- await act(() => {
237
- resolveMostRecentTextCache('A');
238
- });
239
- assertLog(['A']);
240
- expect(root).toMatchRenderedOutput('A');
241
-
242
- await act(() => {
243
- root.render('Bye');
244
- });
245
- // no cleanup: cache is still retained at the root
246
- assertLog([]);
247
- expect(root).toMatchRenderedOutput('Bye');
248
- });
249
-
250
- // @gate enableCacheElement && enableCache
251
- test('multiple new Cache boundaries in the same mount share the same, fresh root cache', async () => {
252
- function App() {
253
- return (
254
- <>
255
- <Cache>
256
- <Suspense fallback={<Text text="Loading..." />}>
257
- <AsyncText text="A" />
258
- </Suspense>
259
- </Cache>
260
- <Cache>
261
- <Suspense fallback={<Text text="Loading..." />}>
262
- <AsyncText text="A" />
263
- </Suspense>
264
- </Cache>
265
- </>
266
- );
267
- }
268
-
269
- const root = ReactNoop.createRoot();
270
- await act(() => {
271
- root.render(<App showMore={false} />);
272
- });
273
-
274
- // Even though there are two new <Cache /> trees, they should share the same
275
- // data cache. So there should be only a single cache miss for A.
276
- assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
277
- expect(root).toMatchRenderedOutput('Loading...Loading...');
278
-
279
- await act(() => {
280
- resolveMostRecentTextCache('A');
281
- });
282
- assertLog(['A', 'A']);
283
- expect(root).toMatchRenderedOutput('AA');
284
-
285
- await act(() => {
286
- root.render('Bye');
287
- });
288
- // no cleanup: cache is still retained at the root
289
- assertLog([]);
290
- expect(root).toMatchRenderedOutput('Bye');
291
- });
292
-
293
- // @gate enableCacheElement && enableCache
294
- test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
295
- function App({showMore}) {
296
- return showMore ? (
297
- <>
298
- <Cache>
299
- <Suspense fallback={<Text text="Loading..." />}>
300
- <AsyncText text="A" />
301
- </Suspense>
302
- </Cache>
303
- <Cache>
304
- <Suspense fallback={<Text text="Loading..." />}>
305
- <AsyncText text="A" />
306
- </Suspense>
307
- </Cache>
308
- </>
309
- ) : (
310
- '(empty)'
311
- );
312
- }
313
-
314
- const root = ReactNoop.createRoot();
315
- await act(() => {
316
- root.render(<App showMore={false} />);
317
- });
318
- assertLog([]);
319
- expect(root).toMatchRenderedOutput('(empty)');
320
-
321
- await act(() => {
322
- root.render(<App showMore={true} />);
323
- });
324
- // Even though there are two new <Cache /> trees, they should share the same
325
- // data cache. So there should be only a single cache miss for A.
326
- assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
327
- expect(root).toMatchRenderedOutput('Loading...Loading...');
328
-
329
- await act(() => {
330
- resolveMostRecentTextCache('A');
331
- });
332
- assertLog(['A', 'A']);
333
- expect(root).toMatchRenderedOutput('AA');
334
-
335
- await act(() => {
336
- root.render('Bye');
337
- });
338
- // cleanup occurs for the cache shared by the inner cache boundaries (which
339
- // are not shared w the root because they were added in an update)
340
- // note that no cache is created for the root since the cache is never accessed
341
- assertLog(['Cache cleanup: A [v1]']);
342
- expect(root).toMatchRenderedOutput('Bye');
343
- });
344
-
345
- // @gate enableCacheElement && enableCache
346
- test(
347
- 'nested cache boundaries share the same cache as the root during ' +
348
- 'the initial render',
349
- async () => {
350
- function App() {
351
- return (
352
- <Suspense fallback={<Text text="Loading..." />}>
353
- <AsyncText text="A" />
354
- <Cache>
355
- <AsyncText text="A" />
356
- </Cache>
357
- </Suspense>
358
- );
359
- }
360
-
361
- const root = ReactNoop.createRoot();
362
- await act(() => {
363
- root.render(<App />);
364
- });
365
- // Even though there is a nested <Cache /> boundary, it should share the same
366
- // data cache as the root. So there should be only a single cache miss for A.
367
- assertLog(['Cache miss! [A]', 'Loading...']);
368
- expect(root).toMatchRenderedOutput('Loading...');
369
-
370
- await act(() => {
371
- resolveMostRecentTextCache('A');
372
- });
373
- assertLog(['A', 'A']);
374
- expect(root).toMatchRenderedOutput('AA');
375
-
376
- await act(() => {
377
- root.render('Bye');
378
- });
379
- // no cleanup: cache is still retained at the root
380
- assertLog([]);
381
- expect(root).toMatchRenderedOutput('Bye');
382
- },
383
- );
384
-
385
- // @gate enableCacheElement && enableCache
386
- test('new content inside an existing Cache boundary should re-use already cached data', async () => {
387
- function App({showMore}) {
388
- return (
389
- <Cache>
390
- <Suspense fallback={<Text text="Loading..." />}>
391
- <AsyncText showVersion={true} text="A" />
392
- </Suspense>
393
- {showMore ? (
394
- <Suspense fallback={<Text text="Loading..." />}>
395
- <AsyncText showVersion={true} text="A" />
396
- </Suspense>
397
- ) : null}
398
- </Cache>
399
- );
400
- }
401
-
402
- const root = ReactNoop.createRoot();
403
- await act(() => {
404
- seedNextTextCache('A');
405
- root.render(<App showMore={false} />);
406
- });
407
- assertLog(['A [v1]']);
408
- expect(root).toMatchRenderedOutput('A [v1]');
409
-
410
- // Add a new cache boundary
411
- await act(() => {
412
- root.render(<App showMore={true} />);
413
- });
414
- assertLog([
415
- 'A [v1]',
416
- // New tree should use already cached data
417
- 'A [v1]',
418
- ]);
419
- expect(root).toMatchRenderedOutput('A [v1]A [v1]');
420
-
421
- await act(() => {
422
- root.render('Bye');
423
- });
424
- // no cleanup: cache is still retained at the root
425
- assertLog([]);
426
- expect(root).toMatchRenderedOutput('Bye');
427
- });
428
-
429
- // @gate enableCacheElement && enableCache
430
- test('a new Cache boundary uses fresh cache', async () => {
431
- // The only difference from the previous test is that the "Show More"
432
- // content is wrapped in a nested <Cache /> boundary
433
- function App({showMore}) {
434
- return (
435
- <Cache>
436
- <Suspense fallback={<Text text="Loading..." />}>
437
- <AsyncText showVersion={true} text="A" />
438
- </Suspense>
439
- {showMore ? (
440
- <Cache>
441
- <Suspense fallback={<Text text="Loading..." />}>
442
- <AsyncText showVersion={true} text="A" />
443
- </Suspense>
444
- </Cache>
445
- ) : null}
446
- </Cache>
447
- );
448
- }
449
-
450
- const root = ReactNoop.createRoot();
451
- await act(() => {
452
- seedNextTextCache('A');
453
- root.render(<App showMore={false} />);
454
- });
455
- assertLog(['A [v1]']);
456
- expect(root).toMatchRenderedOutput('A [v1]');
457
-
458
- // Add a new cache boundary
459
- await act(() => {
460
- root.render(<App showMore={true} />);
461
- });
462
- assertLog([
463
- 'A [v1]',
464
- // New tree should load fresh data.
465
- 'Cache miss! [A]',
466
- 'Loading...',
467
- ]);
468
- expect(root).toMatchRenderedOutput('A [v1]Loading...');
469
- await act(() => {
470
- resolveMostRecentTextCache('A');
471
- });
472
- assertLog(['A [v2]']);
473
- expect(root).toMatchRenderedOutput('A [v1]A [v2]');
474
-
475
- // Replace all the children: this should retain the root Cache instance,
476
- // but cleanup the separate cache instance created for the fresh cache
477
- // boundary
478
- await act(() => {
479
- root.render('Bye!');
480
- });
481
- // Cleanup occurs for the *second* cache instance: the first is still
482
- // referenced by the root
483
- assertLog(['Cache cleanup: A [v2]']);
484
- expect(root).toMatchRenderedOutput('Bye!');
485
- });
486
-
487
- // @gate enableCacheElement && enableCache
488
- test('inner/outer cache boundaries uses the same cache instance on initial render', async () => {
489
- const root = ReactNoop.createRoot();
490
-
491
- function App() {
492
- return (
493
- <Cache>
494
- <Suspense fallback={<Text text="Loading shell..." />}>
495
- {/* The shell reads A */}
496
- <Shell>
497
- {/* The inner content reads both A and B */}
498
- <Suspense fallback={<Text text="Loading content..." />}>
499
- <Cache>
500
- <Content />
501
- </Cache>
502
- </Suspense>
503
- </Shell>
504
- </Suspense>
505
- </Cache>
506
- );
507
- }
508
-
509
- function Shell({children}) {
510
- readText('A');
511
- return (
512
- <>
513
- <div>
514
- <Text text="Shell" />
515
- </div>
516
- <div>{children}</div>
517
- </>
518
- );
519
- }
520
-
521
- function Content() {
522
- readText('A');
523
- readText('B');
524
- return <Text text="Content" />;
525
- }
526
-
527
- await act(() => {
528
- root.render(<App />);
529
- });
530
- assertLog(['Cache miss! [A]', 'Loading shell...']);
531
- expect(root).toMatchRenderedOutput('Loading shell...');
532
-
533
- await act(() => {
534
- resolveMostRecentTextCache('A');
535
- });
536
- assertLog([
537
- 'Shell',
538
- // There's a cache miss for B, because it hasn't been read yet. But not
539
- // A, because it was cached when we rendered the shell.
540
- 'Cache miss! [B]',
541
- 'Loading content...',
542
- ]);
543
- expect(root).toMatchRenderedOutput(
544
- <>
545
- <div>Shell</div>
546
- <div>Loading content...</div>
547
- </>,
548
- );
549
-
550
- await act(() => {
551
- resolveMostRecentTextCache('B');
552
- });
553
- assertLog(['Content']);
554
- expect(root).toMatchRenderedOutput(
555
- <>
556
- <div>Shell</div>
557
- <div>Content</div>
558
- </>,
559
- );
560
-
561
- await act(() => {
562
- root.render('Bye');
563
- });
564
- // no cleanup: cache is still retained at the root
565
- assertLog([]);
566
- expect(root).toMatchRenderedOutput('Bye');
567
- });
568
-
569
- // @gate enableCacheElement && enableCache
570
- test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => {
571
- const root = ReactNoop.createRoot();
572
-
573
- function App({showMore}) {
574
- return showMore ? (
575
- <Cache>
576
- <Suspense fallback={<Text text="Loading shell..." />}>
577
- {/* The shell reads A */}
578
- <Shell>
579
- {/* The inner content reads both A and B */}
580
- <Suspense fallback={<Text text="Loading content..." />}>
581
- <Cache>
582
- <Content />
583
- </Cache>
584
- </Suspense>
585
- </Shell>
586
- </Suspense>
587
- </Cache>
588
- ) : (
589
- '(empty)'
590
- );
591
- }
592
-
593
- function Shell({children}) {
594
- readText('A');
595
- return (
596
- <>
597
- <div>
598
- <Text text="Shell" />
599
- </div>
600
- <div>{children}</div>
601
- </>
602
- );
603
- }
604
-
605
- function Content() {
606
- readText('A');
607
- readText('B');
608
- return <Text text="Content" />;
609
- }
610
-
611
- await act(() => {
612
- root.render(<App showMore={false} />);
613
- });
614
- assertLog([]);
615
- expect(root).toMatchRenderedOutput('(empty)');
616
-
617
- await act(() => {
618
- root.render(<App showMore={true} />);
619
- });
620
- assertLog(['Cache miss! [A]', 'Loading shell...']);
621
- expect(root).toMatchRenderedOutput('Loading shell...');
622
-
623
- await act(() => {
624
- resolveMostRecentTextCache('A');
625
- });
626
- assertLog([
627
- 'Shell',
628
- // There's a cache miss for B, because it hasn't been read yet. But not
629
- // A, because it was cached when we rendered the shell.
630
- 'Cache miss! [B]',
631
- 'Loading content...',
632
- ]);
633
- expect(root).toMatchRenderedOutput(
634
- <>
635
- <div>Shell</div>
636
- <div>Loading content...</div>
637
- </>,
638
- );
639
-
640
- await act(() => {
641
- resolveMostRecentTextCache('B');
642
- });
643
- assertLog(['Content']);
644
- expect(root).toMatchRenderedOutput(
645
- <>
646
- <div>Shell</div>
647
- <div>Content</div>
648
- </>,
649
- );
650
-
651
- await act(() => {
652
- root.render('Bye');
653
- });
654
- assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: B [v1]']);
655
- expect(root).toMatchRenderedOutput('Bye');
656
- });
657
-
658
- // @gate enableCache
659
- test('refresh a cache boundary', async () => {
660
- let refresh;
661
- function App() {
662
- refresh = useCacheRefresh();
663
- return <AsyncText showVersion={true} text="A" />;
664
- }
665
-
666
- // Mount initial data
667
- const root = ReactNoop.createRoot();
668
- await act(() => {
669
- root.render(
670
- <Suspense fallback={<Text text="Loading..." />}>
671
- <App />
672
- </Suspense>,
673
- );
674
- });
675
- assertLog(['Cache miss! [A]', 'Loading...']);
676
- expect(root).toMatchRenderedOutput('Loading...');
677
-
678
- await act(() => {
679
- resolveMostRecentTextCache('A');
680
- });
681
- assertLog(['A [v1]']);
682
- expect(root).toMatchRenderedOutput('A [v1]');
683
-
684
- // Refresh for new data.
685
- await act(() => {
686
- startTransition(() => refresh());
687
- });
688
- assertLog(['Cache miss! [A]', 'Loading...']);
689
- expect(root).toMatchRenderedOutput('A [v1]');
690
-
691
- await act(() => {
692
- resolveMostRecentTextCache('A');
693
- });
694
- // Note that the version has updated
695
- if (getCacheSignal) {
696
- assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
697
- } else {
698
- assertLog(['A [v2]']);
699
- }
700
- expect(root).toMatchRenderedOutput('A [v2]');
701
-
702
- await act(() => {
703
- root.render('Bye');
704
- });
705
- expect(root).toMatchRenderedOutput('Bye');
706
- });
707
-
708
- // @gate enableCacheElement && enableCache
709
- test('refresh the root cache', async () => {
710
- let refresh;
711
- function App() {
712
- refresh = useCacheRefresh();
713
- return <AsyncText showVersion={true} text="A" />;
714
- }
715
-
716
- // Mount initial data
717
- const root = ReactNoop.createRoot();
718
- await act(() => {
719
- root.render(
720
- <Suspense fallback={<Text text="Loading..." />}>
721
- <App />
722
- </Suspense>,
723
- );
724
- });
725
- assertLog(['Cache miss! [A]', 'Loading...']);
726
- expect(root).toMatchRenderedOutput('Loading...');
727
-
728
- await act(() => {
729
- resolveMostRecentTextCache('A');
730
- });
731
- assertLog(['A [v1]']);
732
- expect(root).toMatchRenderedOutput('A [v1]');
733
-
734
- // Refresh for new data.
735
- await act(() => {
736
- startTransition(() => refresh());
737
- });
738
- assertLog(['Cache miss! [A]', 'Loading...']);
739
- expect(root).toMatchRenderedOutput('A [v1]');
740
-
741
- await act(() => {
742
- resolveMostRecentTextCache('A');
743
- });
744
- // Note that the version has updated, and the previous cache is cleared
745
- assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
746
- expect(root).toMatchRenderedOutput('A [v2]');
747
-
748
- await act(() => {
749
- root.render('Bye');
750
- });
751
- // the original root cache already cleaned up when the refresh completed
752
- assertLog([]);
753
- expect(root).toMatchRenderedOutput('Bye');
754
- });
755
-
756
- // @gate enableCacheElement && enableCache
757
- test('refresh the root cache without a transition', async () => {
758
- let refresh;
759
- function App() {
760
- refresh = useCacheRefresh();
761
- return <AsyncText showVersion={true} text="A" />;
762
- }
763
-
764
- // Mount initial data
765
- const root = ReactNoop.createRoot();
766
- await act(() => {
767
- root.render(
768
- <Suspense fallback={<Text text="Loading..." />}>
769
- <App />
770
- </Suspense>,
771
- );
772
- });
773
- assertLog(['Cache miss! [A]', 'Loading...']);
774
- expect(root).toMatchRenderedOutput('Loading...');
775
-
776
- await act(() => {
777
- resolveMostRecentTextCache('A');
778
- });
779
- assertLog(['A [v1]']);
780
- expect(root).toMatchRenderedOutput('A [v1]');
781
-
782
- // Refresh for new data.
783
- await act(() => {
784
- refresh();
785
- });
786
- assertLog([
787
- 'Cache miss! [A]',
788
- 'Loading...',
789
- // The v1 cache can be cleaned up since everything that references it has
790
- // been replaced by a fallback. When the boundary switches back to visible
791
- // it will use the v2 cache.
792
- 'Cache cleanup: A [v1]',
793
- ]);
794
- expect(root).toMatchRenderedOutput('Loading...');
795
-
796
- await act(() => {
797
- resolveMostRecentTextCache('A');
798
- });
799
- // Note that the version has updated, and the previous cache is cleared
800
- assertLog(['A [v2]']);
801
- expect(root).toMatchRenderedOutput('A [v2]');
802
-
803
- await act(() => {
804
- root.render('Bye');
805
- });
806
- // the original root cache already cleaned up when the refresh completed
807
- assertLog([]);
808
- expect(root).toMatchRenderedOutput('Bye');
809
- });
810
-
811
- // @gate enableCacheElement && enableCache
812
- test('refresh a cache with seed data', async () => {
813
- let refreshWithSeed;
814
- function App() {
815
- const refresh = useCacheRefresh();
816
- const [seed, setSeed] = useState({fn: null});
817
- if (seed.fn) {
818
- seed.fn();
819
- seed.fn = null;
820
- }
821
- refreshWithSeed = fn => {
822
- setSeed({fn});
823
- refresh();
824
- };
825
- return <AsyncText showVersion={true} text="A" />;
826
- }
827
-
828
- // Mount initial data
829
- const root = ReactNoop.createRoot();
830
- await act(() => {
831
- root.render(
832
- <Cache>
833
- <Suspense fallback={<Text text="Loading..." />}>
834
- <App />
835
- </Suspense>
836
- </Cache>,
837
- );
838
- });
839
- assertLog(['Cache miss! [A]', 'Loading...']);
840
- expect(root).toMatchRenderedOutput('Loading...');
841
-
842
- await act(() => {
843
- resolveMostRecentTextCache('A');
844
- });
845
- assertLog(['A [v1]']);
846
- expect(root).toMatchRenderedOutput('A [v1]');
847
-
848
- // Refresh for new data.
849
- await act(() => {
850
- // Refresh the cache with seeded data, like you would receive from a
851
- // server mutation.
852
- // TODO: Seeding multiple typed textCaches. Should work by calling `refresh`
853
- // multiple times with different key/value pairs
854
- startTransition(() =>
855
- refreshWithSeed(() => {
856
- const textCache = getTextCache();
857
- textCache.resolve('A');
858
- }),
859
- );
860
- });
861
- // The root should re-render without a cache miss.
862
- // The cache is not cleared up yet, since it's still reference by the root
863
- assertLog(['A [v2]']);
864
- expect(root).toMatchRenderedOutput('A [v2]');
865
-
866
- await act(() => {
867
- root.render('Bye');
868
- });
869
- // the refreshed cache boundary is unmounted and cleans up
870
- assertLog(['Cache cleanup: A [v2]']);
871
- expect(root).toMatchRenderedOutput('Bye');
872
- });
873
-
874
- // @gate enableCacheElement && enableCache
875
- test('refreshing a parent cache also refreshes its children', async () => {
876
- let refreshShell;
877
- function RefreshShell() {
878
- refreshShell = useCacheRefresh();
879
- return null;
880
- }
881
-
882
- function App({showMore}) {
883
- return (
884
- <Cache>
885
- <RefreshShell />
886
- <Suspense fallback={<Text text="Loading..." />}>
887
- <AsyncText showVersion={true} text="A" />
888
- </Suspense>
889
- {showMore ? (
890
- <Cache>
891
- <Suspense fallback={<Text text="Loading..." />}>
892
- <AsyncText showVersion={true} text="A" />
893
- </Suspense>
894
- </Cache>
895
- ) : null}
896
- </Cache>
897
- );
898
- }
899
-
900
- const root = ReactNoop.createRoot();
901
- await act(() => {
902
- seedNextTextCache('A');
903
- root.render(<App showMore={false} />);
904
- });
905
- assertLog(['A [v1]']);
906
- expect(root).toMatchRenderedOutput('A [v1]');
907
-
908
- // Add a new cache boundary
909
- await act(() => {
910
- seedNextTextCache('A');
911
- root.render(<App showMore={true} />);
912
- });
913
- assertLog([
914
- 'A [v1]',
915
- // New tree should load fresh data.
916
- 'A [v2]',
917
- ]);
918
- expect(root).toMatchRenderedOutput('A [v1]A [v2]');
919
-
920
- // Now refresh the shell. This should also cause the "Show More" contents to
921
- // refresh, since its cache is nested inside the outer one.
922
- await act(() => {
923
- startTransition(() => refreshShell());
924
- });
925
- assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
926
- expect(root).toMatchRenderedOutput('A [v1]A [v2]');
927
-
928
- await act(() => {
929
- resolveMostRecentTextCache('A');
930
- });
931
- assertLog([
932
- 'A [v3]',
933
- 'A [v3]',
934
- // once the refresh completes the inner showMore boundary frees its previous
935
- // cache instance, since it is now using the refreshed parent instance.
936
- 'Cache cleanup: A [v2]',
937
- ]);
938
- expect(root).toMatchRenderedOutput('A [v3]A [v3]');
939
-
940
- await act(() => {
941
- root.render('Bye!');
942
- });
943
- // Unmounting children releases the refreshed cache instance only; the root
944
- // still retains the original cache instance used for the first render
945
- assertLog(['Cache cleanup: A [v3]']);
946
- expect(root).toMatchRenderedOutput('Bye!');
947
- });
948
-
949
- // @gate enableCacheElement && enableCache
950
- test(
951
- 'refreshing a cache boundary does not refresh the other boundaries ' +
952
- 'that mounted at the same time (i.e. the ones that share the same cache)',
953
- async () => {
954
- let refreshFirstBoundary;
955
- function RefreshFirstBoundary() {
956
- refreshFirstBoundary = useCacheRefresh();
957
- return null;
958
- }
959
-
960
- function App({showMore}) {
961
- return showMore ? (
962
- <>
963
- <Cache>
964
- <Suspense fallback={<Text text="Loading..." />}>
965
- <RefreshFirstBoundary />
966
- <AsyncText showVersion={true} text="A" />
967
- </Suspense>
968
- </Cache>
969
- <Cache>
970
- <Suspense fallback={<Text text="Loading..." />}>
971
- <AsyncText showVersion={true} text="A" />
972
- </Suspense>
973
- </Cache>
974
- </>
975
- ) : null;
976
- }
977
-
978
- // First mount the initial shell without the nested boundaries. This is
979
- // necessary for this test because we want the two inner boundaries to be
980
- // treated like sibling providers that happen to share an underlying
981
- // cache, as opposed to consumers of the root-level cache.
982
- const root = ReactNoop.createRoot();
983
- await act(() => {
984
- root.render(<App showMore={false} />);
985
- });
986
-
987
- // Now reveal the boundaries. In a real app this would be a navigation.
988
- await act(() => {
989
- root.render(<App showMore={true} />);
990
- });
991
-
992
- // Even though there are two new <Cache /> trees, they should share the same
993
- // data cache. So there should be only a single cache miss for A.
994
- assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
995
- expect(root).toMatchRenderedOutput('Loading...Loading...');
996
-
997
- await act(() => {
998
- resolveMostRecentTextCache('A');
999
- });
1000
- assertLog(['A [v1]', 'A [v1]']);
1001
- expect(root).toMatchRenderedOutput('A [v1]A [v1]');
1002
-
1003
- // Refresh the first boundary. It should not refresh the second boundary,
1004
- // even though they previously shared the same underlying cache.
1005
- await act(async () => {
1006
- await refreshFirstBoundary();
1007
- });
1008
- assertLog(['Cache miss! [A]', 'Loading...']);
1009
-
1010
- await act(() => {
1011
- resolveMostRecentTextCache('A');
1012
- });
1013
- assertLog(['A [v2]']);
1014
- expect(root).toMatchRenderedOutput('A [v2]A [v1]');
1015
-
1016
- // Unmount children: this should clear *both* cache instances:
1017
- // the root doesn't have a cache instance (since it wasn't accessed
1018
- // during the initial render, and all subsequent cache accesses were within
1019
- // a fresh boundary). Therefore this causes cleanup for both the fresh cache
1020
- // instance in the refreshed first boundary and cleanup for the non-refreshed
1021
- // sibling boundary.
1022
- await act(() => {
1023
- root.render('Bye!');
1024
- });
1025
- assertLog(['Cache cleanup: A [v2]', 'Cache cleanup: A [v1]']);
1026
- expect(root).toMatchRenderedOutput('Bye!');
1027
- },
1028
- );
1029
-
1030
- // @gate enableCacheElement && enableCache
1031
- test(
1032
- 'mount a new Cache boundary in a sibling while simultaneously ' +
1033
- 'resolving a Suspense boundary',
1034
- async () => {
1035
- function App({showMore}) {
1036
- return (
1037
- <>
1038
- {showMore ? (
1039
- <Suspense fallback={<Text text="Loading..." />}>
1040
- <Cache>
1041
- <AsyncText showVersion={true} text="A" />
1042
- </Cache>
1043
- </Suspense>
1044
- ) : null}
1045
- <Suspense fallback={<Text text="Loading..." />}>
1046
- <Cache>
1047
- {' '}
1048
- <AsyncText showVersion={true} text="A" />{' '}
1049
- <AsyncText showVersion={true} text="B" />
1050
- </Cache>
1051
- </Suspense>
1052
- </>
1053
- );
1054
- }
1055
-
1056
- const root = ReactNoop.createRoot();
1057
- await act(() => {
1058
- root.render(<App showMore={false} />);
1059
- });
1060
- assertLog(['Cache miss! [A]', 'Loading...']);
1061
- expect(root).toMatchRenderedOutput('Loading...');
1062
-
1063
- await act(() => {
1064
- // This will resolve the content in the first cache
1065
- resolveMostRecentTextCache('A');
1066
- resolveMostRecentTextCache('B');
1067
- // And mount the second tree, which includes new content
1068
- root.render(<App showMore={true} />);
1069
- });
1070
- assertLog([
1071
- // The new tree should use a fresh cache
1072
- 'Cache miss! [A]',
1073
- 'Loading...',
1074
- // The other tree uses the cached responses. This demonstrates that the
1075
- // requests are not dropped.
1076
- 'A [v1]',
1077
- 'B [v1]',
1078
- ]);
1079
- expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]');
1080
-
1081
- // Now resolve the second tree
1082
- await act(() => {
1083
- resolveMostRecentTextCache('A');
1084
- });
1085
- assertLog(['A [v2]']);
1086
- expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
1087
-
1088
- await act(() => {
1089
- root.render('Bye!');
1090
- });
1091
- // Unmounting children releases both cache boundaries, but the original
1092
- // cache instance (used by second boundary) is still referenced by the root.
1093
- // only the second cache instance is freed.
1094
- assertLog(['Cache cleanup: A [v2]']);
1095
- expect(root).toMatchRenderedOutput('Bye!');
1096
- },
1097
- );
1098
-
1099
- // @gate enableCacheElement && enableCache
1100
- test('cache pool is cleared once transitions that depend on it commit their shell', async () => {
1101
- function Child({text}) {
1102
- return (
1103
- <Cache>
1104
- <AsyncText showVersion={true} text={text} />
1105
- </Cache>
1106
- );
1107
- }
1108
-
1109
- const root = ReactNoop.createRoot();
1110
- await act(() => {
1111
- root.render(
1112
- <Suspense fallback={<Text text="Loading..." />}>(empty)</Suspense>,
1113
- );
1114
- });
1115
- assertLog([]);
1116
- expect(root).toMatchRenderedOutput('(empty)');
1117
-
1118
- await act(() => {
1119
- startTransition(() => {
1120
- root.render(
1121
- <Suspense fallback={<Text text="Loading..." />}>
1122
- <Child text="A" />
1123
- </Suspense>,
1124
- );
1125
- });
1126
- });
1127
- assertLog(['Cache miss! [A]', 'Loading...']);
1128
- expect(root).toMatchRenderedOutput('(empty)');
1129
-
1130
- await act(() => {
1131
- startTransition(() => {
1132
- root.render(
1133
- <Suspense fallback={<Text text="Loading..." />}>
1134
- <Child text="A" />
1135
- <Child text="A" />
1136
- </Suspense>,
1137
- );
1138
- });
1139
- });
1140
- assertLog([
1141
- // No cache miss, because it uses the pooled cache
1142
- 'Loading...',
1143
- ]);
1144
- expect(root).toMatchRenderedOutput('(empty)');
1145
-
1146
- // Resolve the request
1147
- await act(() => {
1148
- resolveMostRecentTextCache('A');
1149
- });
1150
- assertLog(['A [v1]', 'A [v1]']);
1151
- expect(root).toMatchRenderedOutput('A [v1]A [v1]');
1152
-
1153
- // Now do another transition
1154
- await act(() => {
1155
- startTransition(() => {
1156
- root.render(
1157
- <Suspense fallback={<Text text="Loading..." />}>
1158
- <Child text="A" />
1159
- <Child text="A" />
1160
- <Child text="A" />
1161
- </Suspense>,
1162
- );
1163
- });
1164
- });
1165
- assertLog([
1166
- // First two children use the old cache because they already finished
1167
- 'A [v1]',
1168
- 'A [v1]',
1169
- // The new child uses a fresh cache
1170
- 'Cache miss! [A]',
1171
- 'Loading...',
1172
- ]);
1173
- expect(root).toMatchRenderedOutput('A [v1]A [v1]');
1174
-
1175
- await act(() => {
1176
- resolveMostRecentTextCache('A');
1177
- });
1178
- assertLog(['A [v1]', 'A [v1]', 'A [v2]']);
1179
- expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
1180
-
1181
- // Unmount children: the first text cache instance is created only after the root
1182
- // commits, so both fresh cache instances are released by their cache boundaries,
1183
- // cleaning up v1 (used for the first two children which render together) and
1184
- // v2 (used for the third boundary added later).
1185
- await act(() => {
1186
- root.render('Bye!');
1187
- });
1188
- assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: A [v2]']);
1189
- expect(root).toMatchRenderedOutput('Bye!');
1190
- });
1191
-
1192
- // @gate enableCacheElement && enableCache
1193
- test('cache pool is not cleared by arbitrary commits', async () => {
1194
- function App() {
1195
- return (
1196
- <>
1197
- <ShowMore />
1198
- <Unrelated />
1199
- </>
1200
- );
1201
- }
1202
-
1203
- let showMore;
1204
- function ShowMore() {
1205
- const [shouldShow, _showMore] = useState(false);
1206
- showMore = () => _showMore(true);
1207
- return (
1208
- <>
1209
- <Suspense fallback={<Text text="Loading..." />}>
1210
- {shouldShow ? (
1211
- <Cache>
1212
- <AsyncText showVersion={true} text="A" />
1213
- </Cache>
1214
- ) : null}
1215
- </Suspense>
1216
- </>
1217
- );
1218
- }
1219
-
1220
- let updateUnrelated;
1221
- function Unrelated() {
1222
- const [count, _updateUnrelated] = useState(0);
1223
- updateUnrelated = _updateUnrelated;
1224
- return <Text text={String(count)} />;
1225
- }
1226
-
1227
- const root = ReactNoop.createRoot();
1228
- await act(() => {
1229
- root.render(<App />);
1230
- });
1231
- assertLog(['0']);
1232
- expect(root).toMatchRenderedOutput('0');
1233
-
1234
- await act(() => {
1235
- startTransition(() => {
1236
- showMore();
1237
- });
1238
- });
1239
- assertLog(['Cache miss! [A]', 'Loading...']);
1240
- expect(root).toMatchRenderedOutput('0');
1241
-
1242
- await act(() => {
1243
- updateUnrelated(1);
1244
- });
1245
- assertLog([
1246
- '1',
1247
-
1248
- // Happens to re-render the fallback. Doesn't need to, but not relevant
1249
- // to this test.
1250
- 'Loading...',
1251
- ]);
1252
- expect(root).toMatchRenderedOutput('1');
1253
-
1254
- await act(() => {
1255
- resolveMostRecentTextCache('A');
1256
- });
1257
- assertLog(['A [v1]']);
1258
- expect(root).toMatchRenderedOutput('A [v1]1');
1259
-
1260
- // Unmount children: the first text cache instance is created only after initial
1261
- // render after calling showMore(). This instance is cleaned up when that boundary
1262
- // is unmounted. Bc root cache instance is never accessed, the inner cache
1263
- // boundary ends up at v1.
1264
- await act(() => {
1265
- root.render('Bye!');
1266
- });
1267
- assertLog(['Cache cleanup: A [v1]']);
1268
- expect(root).toMatchRenderedOutput('Bye!');
1269
- });
1270
-
1271
- // @gate enableCacheElement && enableCache
1272
- test('cache boundary uses a fresh cache when its key changes', async () => {
1273
- const root = ReactNoop.createRoot();
1274
- seedNextTextCache('A');
1275
- await act(() => {
1276
- root.render(
1277
- <Suspense fallback="Loading...">
1278
- <Cache key="A">
1279
- <AsyncText showVersion={true} text="A" />
1280
- </Cache>
1281
- </Suspense>,
1282
- );
1283
- });
1284
- assertLog(['A [v1]']);
1285
- expect(root).toMatchRenderedOutput('A [v1]');
1286
-
1287
- seedNextTextCache('B');
1288
- await act(() => {
1289
- root.render(
1290
- <Suspense fallback="Loading...">
1291
- <Cache key="B">
1292
- <AsyncText showVersion={true} text="B" />
1293
- </Cache>
1294
- </Suspense>,
1295
- );
1296
- });
1297
- assertLog(['B [v2]']);
1298
- expect(root).toMatchRenderedOutput('B [v2]');
1299
-
1300
- // Unmount children: the fresh cache instance for B cleans up since the cache boundary
1301
- // is the only owner, while the original cache instance (for A) is still retained by
1302
- // the root.
1303
- await act(() => {
1304
- root.render('Bye!');
1305
- });
1306
- assertLog(['Cache cleanup: B [v2]']);
1307
- expect(root).toMatchRenderedOutput('Bye!');
1308
- });
1309
-
1310
- // @gate enableCacheElement && enableCache
1311
- test('overlapping transitions after an initial mount use the same fresh cache', async () => {
1312
- const root = ReactNoop.createRoot();
1313
- await act(() => {
1314
- root.render(
1315
- <Suspense fallback="Loading...">
1316
- <Cache key="A">
1317
- <AsyncText showVersion={true} text="A" />
1318
- </Cache>
1319
- </Suspense>,
1320
- );
1321
- });
1322
- assertLog(['Cache miss! [A]']);
1323
- expect(root).toMatchRenderedOutput('Loading...');
1324
-
1325
- await act(() => {
1326
- resolveMostRecentTextCache('A');
1327
- });
1328
- assertLog(['A [v1]']);
1329
- expect(root).toMatchRenderedOutput('A [v1]');
1330
-
1331
- // After a mount, subsequent transitions use a fresh cache
1332
- await act(() => {
1333
- startTransition(() => {
1334
- root.render(
1335
- <Suspense fallback="Loading...">
1336
- <Cache key="B">
1337
- <AsyncText showVersion={true} text="B" />
1338
- </Cache>
1339
- </Suspense>,
1340
- );
1341
- });
1342
- });
1343
- assertLog(['Cache miss! [B]']);
1344
- expect(root).toMatchRenderedOutput('A [v1]');
1345
-
1346
- // Update to a different text and with a different key for the cache
1347
- // boundary: this should still use the fresh cache instance created
1348
- // for the earlier transition
1349
- await act(() => {
1350
- startTransition(() => {
1351
- root.render(
1352
- <Suspense fallback="Loading...">
1353
- <Cache key="C">
1354
- <AsyncText showVersion={true} text="C" />
1355
- </Cache>
1356
- </Suspense>,
1357
- );
1358
- });
1359
- });
1360
- assertLog(['Cache miss! [C]']);
1361
- expect(root).toMatchRenderedOutput('A [v1]');
1362
-
1363
- await act(() => {
1364
- resolveMostRecentTextCache('C');
1365
- });
1366
- assertLog(['C [v2]']);
1367
- expect(root).toMatchRenderedOutput('C [v2]');
1368
-
1369
- // Unmount children: the fresh cache used for the updates is freed, while the
1370
- // original cache (with A) is still retained at the root.
1371
- await act(() => {
1372
- root.render('Bye!');
1373
- });
1374
- assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']);
1375
- expect(root).toMatchRenderedOutput('Bye!');
1376
- });
1377
-
1378
- // @gate enableCacheElement && enableCache
1379
- test('overlapping updates after an initial mount use the same fresh cache', async () => {
1380
- const root = ReactNoop.createRoot();
1381
- await act(() => {
1382
- root.render(
1383
- <Suspense fallback="Loading...">
1384
- <Cache key="A">
1385
- <AsyncText showVersion={true} text="A" />
1386
- </Cache>
1387
- </Suspense>,
1388
- );
1389
- });
1390
- assertLog(['Cache miss! [A]']);
1391
- expect(root).toMatchRenderedOutput('Loading...');
1392
-
1393
- await act(() => {
1394
- resolveMostRecentTextCache('A');
1395
- });
1396
- assertLog(['A [v1]']);
1397
- expect(root).toMatchRenderedOutput('A [v1]');
1398
-
1399
- // After a mount, subsequent updates use a fresh cache
1400
- await act(() => {
1401
- root.render(
1402
- <Suspense fallback="Loading...">
1403
- <Cache key="B">
1404
- <AsyncText showVersion={true} text="B" />
1405
- </Cache>
1406
- </Suspense>,
1407
- );
1408
- });
1409
- assertLog(['Cache miss! [B]']);
1410
- expect(root).toMatchRenderedOutput('Loading...');
1411
-
1412
- // A second update uses the same fresh cache: even though this is a new
1413
- // Cache boundary, the render uses the fresh cache from the pending update.
1414
- await act(() => {
1415
- root.render(
1416
- <Suspense fallback="Loading...">
1417
- <Cache key="C">
1418
- <AsyncText showVersion={true} text="C" />
1419
- </Cache>
1420
- </Suspense>,
1421
- );
1422
- });
1423
- assertLog(['Cache miss! [C]']);
1424
- expect(root).toMatchRenderedOutput('Loading...');
1425
-
1426
- await act(() => {
1427
- resolveMostRecentTextCache('C');
1428
- });
1429
- assertLog(['C [v2]']);
1430
- expect(root).toMatchRenderedOutput('C [v2]');
1431
-
1432
- // Unmount children: the fresh cache used for the updates is freed, while the
1433
- // original cache (with A) is still retained at the root.
1434
- await act(() => {
1435
- root.render('Bye!');
1436
- });
1437
- assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']);
1438
- expect(root).toMatchRenderedOutput('Bye!');
1439
- });
1440
-
1441
- // @gate enableCacheElement && enableCache
1442
- test('cleans up cache only used in an aborted transition', async () => {
1443
- const root = ReactNoop.createRoot();
1444
- seedNextTextCache('A');
1445
- await act(() => {
1446
- root.render(
1447
- <Suspense fallback="Loading...">
1448
- <Cache key="A">
1449
- <AsyncText showVersion={true} text="A" />
1450
- </Cache>
1451
- </Suspense>,
1452
- );
1453
- });
1454
- assertLog(['A [v1]']);
1455
- expect(root).toMatchRenderedOutput('A [v1]');
1456
-
1457
- // Start a transition from A -> B..., which should create a fresh cache
1458
- // for the new cache boundary (bc of the different key)
1459
- await act(() => {
1460
- startTransition(() => {
1461
- root.render(
1462
- <Suspense fallback="Loading...">
1463
- <Cache key="B">
1464
- <AsyncText showVersion={true} text="B" />
1465
- </Cache>
1466
- </Suspense>,
1467
- );
1468
- });
1469
- });
1470
- assertLog(['Cache miss! [B]']);
1471
- expect(root).toMatchRenderedOutput('A [v1]');
1472
-
1473
- // ...but cancel by transitioning "back" to A (which we never really left)
1474
- await act(() => {
1475
- startTransition(() => {
1476
- root.render(
1477
- <Suspense fallback="Loading...">
1478
- <Cache key="A">
1479
- <AsyncText showVersion={true} text="A" />
1480
- </Cache>
1481
- </Suspense>,
1482
- );
1483
- });
1484
- });
1485
- assertLog(['A [v1]', 'Cache cleanup: B [v2]']);
1486
- expect(root).toMatchRenderedOutput('A [v1]');
1487
-
1488
- // Unmount children: ...
1489
- await act(() => {
1490
- root.render('Bye!');
1491
- });
1492
- assertLog([]);
1493
- expect(root).toMatchRenderedOutput('Bye!');
1494
- });
1495
-
1496
- // @gate enableCacheElement && enableCache
1497
- test.skip('if a root cache refresh never commits its fresh cache is released', async () => {
1498
- const root = ReactNoop.createRoot();
1499
- let refresh;
1500
- function Example({text}) {
1501
- refresh = useCacheRefresh();
1502
- return <AsyncText showVersion={true} text={text} />;
1503
- }
1504
- seedNextTextCache('A');
1505
- await act(() => {
1506
- root.render(
1507
- <Suspense fallback="Loading...">
1508
- <Example text="A" />
1509
- </Suspense>,
1510
- );
1511
- });
1512
- assertLog(['A [v1]']);
1513
- expect(root).toMatchRenderedOutput('A [v1]');
1514
-
1515
- await act(() => {
1516
- startTransition(() => {
1517
- refresh();
1518
- });
1519
- });
1520
- assertLog(['Cache miss! [A]']);
1521
- expect(root).toMatchRenderedOutput('A [v1]');
1522
-
1523
- await act(() => {
1524
- root.render('Bye!');
1525
- });
1526
- assertLog([
1527
- // TODO: the v1 cache should *not* be cleaned up, it is still retained by the root
1528
- // The following line is presently yielded but should not be:
1529
- // 'Cache cleanup: A [v1]',
23
1531
- // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
1532
- // The following line is presently not yielded but should be:
1533
- 'Cache cleanup: A [v2]',
1534
- ]);
1535
- expect(root).toMatchRenderedOutput('Bye!');
1536
- });
24
+ ReactNoopFlightServer = require('react-noop-renderer/flight-server');
25
+ ReactNoopFlightClient = require('react-noop-renderer/flight-client');
26
1538
- // @gate enableCacheElement && enableCache
1539
- test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => {
1540
- const root = ReactNoop.createRoot();
1541
- let refresh;
1542
- function Example({text}) {
1543
- refresh = useCacheRefresh();
1544
- return <AsyncText showVersion={true} text={text} />;
1545
- }
1546
- seedNextTextCache('A');
1547
- await act(() => {
1548
- root.render(
1549
- <Suspense fallback="Loading...">
1550
- <Cache>
1551
- <Example text="A" />
1552
- </Cache>
1553
- </Suspense>,
1554
- );
1555
- });
1556
- assertLog(['A [v1]']);
1557
- expect(root).toMatchRenderedOutput('A [v1]');
1558
-
1559
- await act(() => {
1560
- startTransition(() => {
1561
- refresh();
1562
- });
1563
- });
1564
- assertLog(['Cache miss! [A]']);
1565
- expect(root).toMatchRenderedOutput('A [v1]');
1566
-
1567
- // Unmount the boundary before the refresh can complete
1568
- await act(() => {
1569
- root.render('Bye!');
1570
- });
1571
- assertLog([
1572
- // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
1573
- // The following line is presently not yielded but should be:
1574
- 'Cache cleanup: A [v2]',
1575
- ]);
1576
- expect(root).toMatchRenderedOutput('Bye!');
1577
- });
1578
-
1579
- // @gate enableActivity
1580
- // @gate enableCache
1581
- test('prerender a new cache boundary inside an Activity tree', async () => {
1582
- function App({prerenderMore}) {
1583
- return (
1584
- <Activity mode="hidden">
1585
- <div>
1586
- {prerenderMore ? (
1587
- <Cache>
1588
- <AsyncText text="More" />
1589
- </Cache>
1590
- ) : null}
1591
- </div>
1592
- </Activity>
1593
- );
1594
- }
1595
-
1596
- const root = ReactNoop.createRoot();
1597
- await act(() => {
1598
- root.render(<App prerenderMore={false} />);
1599
- });
1600
- assertLog([]);
1601
- expect(root).toMatchRenderedOutput(<div hidden={true} />);
1602
-
1603
- seedNextTextCache('More');
1604
- await act(() => {
1605
- root.render(<App prerenderMore={true} />);
1606
- });
1607
- assertLog(['More']);
1608
- expect(root).toMatchRenderedOutput(<div hidden={true}>More</div>);
27
+ cache = React.cache;
28
});
29
30
// @gate enableCache
31
it('cache objects and primitive arguments and a mix of them', async () => {
1613
- const root = ReactNoop.createRoot();
32
const types = cache((a, b) => ({a: typeof a, b: typeof b}));
33
function Print({a, b}) {
34
return types(a, b).a + ' ' + types(a, b).b + ' ';
47
function MoreArgs({a, b}) {
48
return (types(a) === types(a, b)).toString() + ' ';
49
}
1632
- await act(() => {
1633
- root.render(
1634
- <>
1635
- <Print a="e" b="f" />
1636
- <Same a="a" b="b" />
1637
- <FlippedOrder a="c" b="d" />
1638
- <FewerArgs a="e" b="f" />
1639
- <MoreArgs a="g" b="h" />
1640
- </>,
1641
- );
1642
- });
1643
- expect(root).toMatchRenderedOutput('string string true false false false ');
1644
- await act(() => {
1645
- root.render(
1646
- <>
1647
- <Print a="e" b={null} />
1648
- <Same a="a" b={null} />
1649
- <FlippedOrder a="c" b={null} />
1650
- <FewerArgs a="e" b={null} />
1651
- <MoreArgs a="g" b={null} />
1652
- </>,
1653
- );
1654
- });
1655
- expect(root).toMatchRenderedOutput('string object true false false false ');
50
+
51
+ expect(
52
+ (
53
+ await ReactNoopFlightClient.read(
54
+ ReactNoopFlightServer.render(
55
+ <>
56
+ <Print a="e" b="f" />
57
+ <Same a="a" b="b" />
58
+ <FlippedOrder a="c" b="d" />
59
+ <FewerArgs a="e" b="f" />
60
+ <MoreArgs a="g" b="h" />
61
+ </>,
62
+ ),
63
+ )
64
+ ).join(''),
65
+ ).toEqual('string string true false false false ');
66
+
67
+ expect(
68
+ (
69
+ await ReactNoopFlightClient.read(
70
+ ReactNoopFlightServer.render(
71
+ <>
72
+ <Print a="e" b={null} />
73
+ <Same a="a" b={null} />
74
+ <FlippedOrder a="c" b={null} />
75
+ <FewerArgs a="e" b={null} />
76
+ <MoreArgs a="g" b={null} />
77
+ </>,
78
+ ),
79
+ )
80
+ ).join(''),
81
+ ).toEqual('string object true false false false ');
82
+
83
const obj = {};
1657
- await act(() => {
1658
- root.render(
1659
- <>
1660
- <Print a="e" b={obj} />
1661
- <Same a="a" b={obj} />
1662
- <FlippedOrder a="c" b={obj} />
1663
- <FewerArgs a="e" b={obj} />
1664
- <MoreArgs a="g" b={obj} />
1665
- </>,
1666
- );
1667
- });
1668
- expect(root).toMatchRenderedOutput('string object true false false false ');
84
+ expect(
85
+ (
86
+ await ReactNoopFlightClient.read(
87
+ ReactNoopFlightServer.render(
88
+ <>
89
+ <Print a="e" b={obj} />
90
+ <Same a="a" b={obj} />
91
+ <FlippedOrder a="c" b={obj} />
92
+ <FewerArgs a="e" b={obj} />
93
+ <MoreArgs a="g" b={obj} />
94
+ </>,
95
+ ),
96
+ )
97
+ ).join(''),
98
+ ).toEqual('string object true false false false ');
99
+
100
const sameObj = {};
1670
- await act(() => {
1671
- root.render(
1672
- <>
1673
- <Print a={sameObj} b={sameObj} />
1674
- <Same a={sameObj} b={sameObj} />
1675
- <FlippedOrder a={sameObj} b={sameObj} />
1676
- <FewerArgs a={sameObj} b={sameObj} />
1677
- <MoreArgs a={sameObj} b={sameObj} />
1678
- </>,
1679
- );
1680
- });
1681
- expect(root).toMatchRenderedOutput('object object true true false false ');
101
+ expect(
102
+ (
103
+ await ReactNoopFlightClient.read(
104
+ ReactNoopFlightServer.render(
105
+ <>
106
+ <Print a={sameObj} b={sameObj} />
107
+ <Same a={sameObj} b={sameObj} />
108
+ <FlippedOrder a={sameObj} b={sameObj} />
109
+ <FewerArgs a={sameObj} b={sameObj} />
110
+ <MoreArgs a={sameObj} b={sameObj} />
111
+ </>,
112
+ ),
113
+ )
114
+ ).join(''),
115
+ ).toEqual('object object true true false false ');
116
+
117
const objA = {};
118
const objB = {};
1684
- await act(() => {
1685
- root.render(
1686
- <>
1687
- <Print a={objA} b={objB} />
1688
- <Same a={objA} b={objB} />
1689
- <FlippedOrder a={objA} b={objB} />
1690
- <FewerArgs a={objA} b={objB} />
1691
- <MoreArgs a={objA} b={objB} />
1692
- </>,
1693
- );
1694
- });
1695
- expect(root).toMatchRenderedOutput('object object true false false false ');
119
+ expect(
120
+ (
121
+ await ReactNoopFlightClient.read(
122
+ ReactNoopFlightServer.render(
123
+ <>
124
+ <Print a={objA} b={objB} />
125
+ <Same a={objA} b={objB} />
126
+ <FlippedOrder a={objA} b={objB} />
127
+ <FewerArgs a={objA} b={objB} />
128
+ <MoreArgs a={objA} b={objB} />
129
+ </>,
130
+ ),
131
+ )
132
+ ).join(''),
133
+ ).toEqual('object object true false false false ');
134
+
135
const sameSymbol = Symbol();
1697
- await act(() => {
1698
- root.render(
1699
- <>
1700
- <Print a={sameSymbol} b={sameSymbol} />
1701
- <Same a={sameSymbol} b={sameSymbol} />
1702
- <FlippedOrder a={sameSymbol} b={sameSymbol} />
1703
- <FewerArgs a={sameSymbol} b={sameSymbol} />
1704
- <MoreArgs a={sameSymbol} b={sameSymbol} />
1705
- </>,
1706
- );
1707
- });
1708
- expect(root).toMatchRenderedOutput('symbol symbol true true false false ');
136
+ expect(
137
+ (
138
+ await ReactNoopFlightClient.read(
139
+ ReactNoopFlightServer.render(
140
+ <>
141
+ <Print a={sameSymbol} b={sameSymbol} />
142
+ <Same a={sameSymbol} b={sameSymbol} />
143
+ <FlippedOrder a={sameSymbol} b={sameSymbol} />
144
+ <FewerArgs a={sameSymbol} b={sameSymbol} />
145
+ <MoreArgs a={sameSymbol} b={sameSymbol} />
146
+ </>,
147
+ ),
148
+ )
149
+ ).join(''),
150
+ ).toEqual('symbol symbol true true false false ');
151
+
152
const notANumber = +'nan';
1710
- await act(() => {
1711
- root.render(
1712
- <>
1713
- <Print a={1} b={notANumber} />
1714
- <Same a={1} b={notANumber} />
1715
- <FlippedOrder a={1} b={notANumber} />
1716
- <FewerArgs a={1} b={notANumber} />
1717
- <MoreArgs a={1} b={notANumber} />
1718
- </>,
1719
- );
1720
- });
1721
- expect(root).toMatchRenderedOutput('number number true false false false ');
153
+ expect(
154
+ (
155
+ await ReactNoopFlightClient.read(
156
+ ReactNoopFlightServer.render(
157
+ <>
158
+ <Print a={1} b={notANumber} />
159
+ <Same a={1} b={notANumber} />
160
+ <FlippedOrder a={1} b={notANumber} />
161
+ <FewerArgs a={1} b={notANumber} />
162
+ <MoreArgs a={1} b={notANumber} />
163
+ </>,
164
+ ),
165
+ )
166
+ ).join(''),
167
+ ).toEqual('number number true false false false ');
168
});
169
170
// @gate enableCache
171
it('cached functions that throw should cache the error', async () => {
1726
- const root = ReactNoop.createRoot();
172
const throws = cache(v => {
173
throw new Error(v);
174
});
194
195
return 'Blank';
196
}
1752
- await act(() => {
1753
- root.render(<Test />);
1754
- });
197
+
198
+ ReactNoopFlightServer.render(<Test />);
199
expect(x).toBe(y);
200
expect(z).not.toBe(x);
201
});
202
+
203
+ // @gate enableCache
204
+ it('introspection of returned wrapper function is same on client and server', async () => {
205
+ // When the variant flag is true, test the client version of `cache`.
206
+ if (gate(flags => flags.variant)) {
207
+ jest.resetModules();
208
+ jest.mock('react', () => jest.requireActual('react'));
209
+ const ClientReact = require('react');
210
+ cache = ClientReact.cache;
211
+ }
212
+
213
+ function foo(a, b, c) {
214
+ return a + b + c;
215
+ }
216
+ foo.displayName = 'Custom display name';
217
+
218
+ const cachedFoo = cache(foo);
219
+ expect(cachedFoo).not.toBe(foo);
220
+ expect(cachedFoo.length).toBe(0);
221
+ expect(cachedFoo.displayName).toBe(undefined);
222
+ });
223
});