Skip to content

Commit 551cdf6

Browse files
committed
Update
1 parent 2942718 commit 551cdf6

2 files changed

Lines changed: 121 additions & 33 deletions

File tree

src/content/reference/react/Suspense.md

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2375,17 +2375,15 @@ The server HTML will include the loading indicator. It will be replaced by the `
23752375
23762376
### Waiting for a stylesheet to load {/*waiting-for-a-stylesheet-to-load*/}
23772377
2378-
A stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop](/reference/react-dom/components/link#special-rendering-behavior) blocks the boundary until the stylesheet loads, up to a timeout, so the content doesn't appear unstyled.
2378+
A stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop](/reference/react-dom/components/link#special-rendering-behavior) blocks the Suspense boundary until the stylesheet loads, up to a timeout, so the content doesn't appear unstyled.
23792379
23802380
In the example below, the `Card` component renders a stylesheet with `precedence`. Press "Show card": React shows the fallback until the stylesheet has loaded, and then reveals the card with its styles applied:
23812381
23822382
<Sandpack>
23832383
23842384
```js
23852385
import { Suspense, useState, startTransition } from 'react';
2386-
2387-
const stylesheet =
2388-
'https://fonts.googleapis.com/css2?family=Caveat&display=block';
2386+
import { freshStylesheetUrl } from './styles.js';
23892387

23902388
function Card({ href }) {
23912389
return (
@@ -2403,9 +2401,7 @@ export default function App() {
24032401
<button
24042402
onClick={() => {
24052403
startTransition(() => {
2406-
// Add a unique parameter so the stylesheet isn't
2407-
// cached, and every run shows the loading state.
2408-
setHref(stylesheet + '&t=' + Date.now());
2404+
setHref(freshStylesheetUrl());
24092405
});
24102406
}}>
24112407
Show card
@@ -2420,6 +2416,18 @@ export default function App() {
24202416
}
24212417
```
24222418
2419+
```js src/styles.js hidden
2420+
// Add a unique parameter so the stylesheet isn't cached,
2421+
// and every run shows the loading state.
2422+
export function freshStylesheetUrl() {
2423+
return (
2424+
'https://fonts.googleapis.com/css2?family=Caveat&display=block' +
2425+
'&t=' +
2426+
Date.now()
2427+
);
2428+
}
2429+
```
2430+
24232431
```css
24242432
#root {
24252433
min-height: 140px;
@@ -2432,7 +2440,7 @@ button {
24322440
padding: 20px;
24332441
border-radius: 8px;
24342442
color: white;
2435-
font-family: 'Caveat', cursive;
2443+
font-family: 'Caveat', sans-serif;
24362444
font-size: 24px;
24372445
background: linear-gradient(135deg, #087ea4, #2b3491);
24382446
}
@@ -2682,18 +2690,19 @@ Where you place the `<ViewTransition>` relative to the boundary determines wheth
26822690
26832691
### Waiting for a font to load {/*waiting-for-a-font-to-load*/}
26842692
2685-
<CanaryBadge /> When a [`<ViewTransition>`](/reference/react/ViewTransition) animates a boundary's reveal, React also waits for new fonts the content introduces, up to a timeout, so the text doesn't flash with a fallback font. This only happens during a `<ViewTransition>` update.
2693+
<CanaryBadge /> When a [`<ViewTransition>`](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React also waits for new fonts the content introduces, up to a timeout, so the text doesn't flash with a fallback font. This only happens during a `<ViewTransition>` update.
2694+
2695+
In the example below, the Suspense boundary is wrapped in a `<ViewTransition>`, and the `Quote` component suspends while its data loads. Rendering the quote starts its font download. React keeps the fallback visible until the font has loaded, so the quote appears already in its font.
26862696
2687-
In the example below, the `Quote` component suspends while its data loads. Rendering the quote starts its font download, so React keeps the fallback visible until the font has loaded, and the quote appears already in its font:
2697+
For comparison, the second button performs the same update with plain DOM, without React. Nothing waits for the font, so the text appears in a fallback font first and then switches:
26882698
26892699
<Sandpack>
26902700
26912701
```js
26922702
import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
26932703
import { fetchQuote } from './data.js';
2694-
2695-
const fontUrl =
2696-
'https://raw.githubusercontent.com/google/fonts/main/ofl/caveat/Caveat%5Bwght%5D.ttf';
2704+
import { freshFontUrl } from './font.js';
2705+
import VanillaQuote from './VanillaQuote.js';
26972706

26982707
function Quote({ fontSrc }) {
26992708
const quote = use(fetchQuote());
@@ -2703,6 +2712,7 @@ function Quote({ fontSrc }) {
27032712
{`@font-face {
27042713
font-family: 'Fancy';
27052714
src: url(${fontSrc}) format('truetype');
2715+
font-display: swap;
27062716
}`}
27072717
</style>
27082718
<p className="quote fancy">{quote}</p>
@@ -2717,9 +2727,7 @@ export default function App() {
27172727
<button
27182728
onClick={() => {
27192729
startTransition(() => {
2720-
// Add a unique parameter so the font isn't cached,
2721-
// and every run shows the font being waited on.
2722-
setFontSrc(fontUrl + '?t=' + Date.now());
2730+
setFontSrc(freshFontUrl());
27232731
});
27242732
}}>
27252733
Show quote
@@ -2731,11 +2739,50 @@ export default function App() {
27312739
</Suspense>
27322740
</ViewTransition>
27332741
)}
2742+
<hr />
2743+
<VanillaQuote />
2744+
</>
2745+
);
2746+
}
2747+
```
2748+
2749+
```js src/VanillaQuote.js
2750+
import { useRef } from 'react';
2751+
import { freshFontUrl } from './font.js';
2752+
2753+
export default function VanillaQuote() {
2754+
const ref = useRef(null);
2755+
function show() {
2756+
const style = document.createElement('style');
2757+
style.textContent = `@font-face {
2758+
font-family: 'VanillaFancy';
2759+
src: url(${freshFontUrl()}) format('truetype');
2760+
font-display: swap;
2761+
}`;
2762+
document.head.appendChild(style);
2763+
ref.current.innerHTML = `<p class="quote vanilla-fancy">The best way to predict the future is to invent it.</p>`;
2764+
}
2765+
return (
2766+
<>
2767+
<button onClick={show}>Show quote (vanilla DOM)</button>
2768+
<div ref={ref} />
27342769
</>
27352770
);
27362771
}
27372772
```
27382773
2774+
```js src/font.js hidden
2775+
// Add a unique parameter so the font isn't cached,
2776+
// and every run shows the font being waited on.
2777+
export function freshFontUrl() {
2778+
return (
2779+
'https://raw.githubusercontent.com/google/fonts/main/ofl/caveat/Caveat%5Bwght%5D.ttf' +
2780+
'?t=' +
2781+
Date.now()
2782+
);
2783+
}
2784+
```
2785+
27392786
```js src/data.js hidden
27402787
// Note: the way you would do data fetching depends on
27412788
// the framework that you use together with Suspense.
@@ -2751,7 +2798,7 @@ export function fetchQuote() {
27512798
resolve(
27522799
'The best way to predict the future is to invent it.'
27532800
);
2754-
}, 1500);
2801+
}, 500);
27552802
});
27562803
}
27572804
return cache;
@@ -2760,14 +2807,20 @@ export function fetchQuote() {
27602807
27612808
```css
27622809
#root {
2763-
min-height: 100px;
2810+
min-height: 260px;
27642811
}
27652812
.quote {
27662813
font-size: 20px;
27672814
margin-top: 1em;
27682815
}
27692816
.fancy {
2770-
font-family: 'Fancy', cursive;
2817+
font-family: 'Fancy', sans-serif;
2818+
}
2819+
.vanilla-fancy {
2820+
font-family: 'VanillaFancy', sans-serif;
2821+
}
2822+
hr {
2823+
margin: 16px 0;
27712824
}
27722825
```
27732826
@@ -2787,22 +2840,24 @@ export function fetchQuote() {
27872840
27882841
### Waiting for an image to load {/*waiting-for-an-image-to-load*/}
27892842
2790-
<CanaryBadge /> Images work the same way: when a [`<ViewTransition>`](/reference/react/ViewTransition) animates a boundary's reveal, React waits for visible images to load, up to a timeout, so the animation doesn't start with a half-loaded image. This only happens during a `<ViewTransition>` update. Adding an `onLoad` handler opts a specific image out, even inside a `<ViewTransition>`.
2843+
<CanaryBadge /> Images work the same way: when a [`<ViewTransition>`](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits for visible images to load, up to a timeout, so the animation doesn't start with a half-loaded image. This only happens during a `<ViewTransition>` update. Adding an `onLoad` handler opts a specific image out, even inside a `<ViewTransition>`.
2844+
2845+
In the example below, the Suspense boundary is wrapped in a `<ViewTransition>` and shows its fallback until the portrait has loaded.
27912846
2792-
In the example below, the boundary shows its fallback until the portrait has loaded:
2847+
For comparison, the second button performs the same update with plain DOM, without React. Nothing waits for the image, so the card appears immediately and the image pops in when it loads:
27932848
27942849
<Sandpack>
27952850
27962851
```js
27972852
import { ViewTransition, Suspense, useState, startTransition } from 'react';
2853+
import { freshImageUrl } from './image.js';
2854+
import VanillaProfile from './VanillaProfile.js';
27982855

2799-
const imageUrl = 'https://react.dev/images/docs/scientists/MK3eW3Am.jpg';
2800-
2801-
function Scientist({ src }) {
2856+
function Profile({ src }) {
28022857
return (
28032858
<div className="card">
2804-
<img src={src} alt="Katherine Johnson" width={100} height={100} />
2805-
<p>Katherine Johnson</p>
2859+
<img src={src} alt="Jack Pope" width={100} height={100} />
2860+
<p>Jack Pope</p>
28062861
</div>
28072862
);
28082863
}
@@ -2814,38 +2869,71 @@ export default function App() {
28142869
<button
28152870
onClick={() => {
28162871
startTransition(() => {
2817-
// Add a unique parameter so the image isn't cached,
2818-
// and every run shows the boundary waiting.
2819-
setSrc(imageUrl + '?t=' + Date.now());
2872+
setSrc(freshImageUrl());
28202873
});
28212874
}}>
2822-
Show scientist
2875+
Show profile
28232876
</button>
28242877
{src && (
28252878
<ViewTransition>
28262879
<Suspense fallback={<p>⌛ Loading image...</p>}>
2827-
<Scientist src={src} />
2880+
<Profile src={src} />
28282881
</Suspense>
28292882
</ViewTransition>
28302883
)}
2884+
<hr />
2885+
<VanillaProfile />
28312886
</>
28322887
);
28332888
}
28342889
```
28352890
2891+
```js src/VanillaProfile.js
2892+
import { useRef } from 'react';
2893+
import { freshImageUrl } from './image.js';
2894+
2895+
export default function VanillaProfile() {
2896+
const ref = useRef(null);
2897+
function show() {
2898+
ref.current.innerHTML = `<div class="card">
2899+
<img src="${freshImageUrl()}" alt="Jack Pope" width="100" height="100" />
2900+
<p>Jack Pope</p>
2901+
</div>`;
2902+
}
2903+
return (
2904+
<>
2905+
<button onClick={show}>Show profile (vanilla DOM)</button>
2906+
<div ref={ref} />
2907+
</>
2908+
);
2909+
}
2910+
```
2911+
2912+
```js src/image.js hidden
2913+
// Add a unique parameter so the image isn't cached,
2914+
// and every run shows the boundary waiting.
2915+
export function freshImageUrl() {
2916+
return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now();
2917+
}
2918+
```
2919+
28362920
```css
28372921
#root {
2838-
min-height: 220px;
2922+
min-height: 440px;
28392923
}
28402924
.card {
28412925
margin-top: 1em;
28422926
}
28432927
.card img {
28442928
border-radius: 50%;
2929+
background: #dfe3e9;
28452930
}
28462931
.card p {
28472932
font-weight: bold;
28482933
}
2934+
hr {
2935+
margin: 16px 0;
2936+
}
28492937
```
28502938
28512939
```json package.json hidden

src/content/reference/react/ViewTransition.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ It's important to properly use keys to preserve identity when reordering lists.
12641264

12651265
### Animating from Suspense content {/*animating-from-suspense-content*/}
12661266

1267-
Like any Transition, React waits for data and new CSS (`<link rel="stylesheet" precedence="...">`) before running the animation. In addition to this, ViewTransitions also wait up to 500ms for new fonts to load before starting the animation to avoid them flickering in later. For the same reason, an image wrapped in ViewTransition will wait for the image to load.
1267+
Like any Transition, React waits for data and new CSS (`<link rel="stylesheet" precedence="...">`) before running the animation. In addition to this, ViewTransitions also wait up to 500ms for new fonts to load before starting the animation to avoid them flickering in later. For the same reason, an image wrapped in ViewTransition will wait for the image to load. See examples of [waiting for a font](/reference/react/Suspense#waiting-for-a-font-to-load) and [waiting for an image](/reference/react/Suspense#waiting-for-an-image-to-load) on the Suspense page.
12681268

12691269
If it's inside a new Suspense boundary instance, then the fallback is shown first. After the Suspense boundary fully loads, it triggers the `<ViewTransition>` to animate the reveal to the content.
12701270

0 commit comments

Comments
 (0)