@samitouri / QOS-React-2 / commits / 5c607369ce

Remove client caching from cache() API (#27977)

We haven't yet decided how we want `cache` to work on the client. The lifetime of the cache is more complex than on the server, where it only has to live as long as a single request. Since it's more important to ship this on the server, we're removing the existing behavior from the client for now. On the client (i.e. not a Server Components environment) `cache` will have not have any caching behavior. `cache(fn)` will return the function as-is. We intend to implement client caching in a future major release. In the meantime, it's only exposed as an API so that Shared Components can use per-request caching on the server without breaking on the client.

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