Skip to content

Commit d63cade

Browse files
committed
Add more examples
1 parent dd5449e commit d63cade

1 file changed

Lines changed: 188 additions & 97 deletions

File tree

src/content/reference/react/Suspense.md

Lines changed: 188 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ A Suspense boundary waits for its content to be ready before revealing it. Any o
4444

4545
- Lazy-loading component code with [`lazy`](/reference/react/lazy).
4646
- Reading a Promise with [`use`](/reference/react/use), including data streamed from [Server Components](/reference/rsc/server-components) or loaded through a [Suspense-enabled framework](#suspense-enabled-frameworks).
47-
- Loading a stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop.](/reference/react-dom/components/link#special-rendering-behavior) React blocks the boundary until the stylesheet loads, up to a timeout.
47+
- Loading a stylesheet rendered with [`<link rel="stylesheet">` and a `precedence` prop.](/reference/react-dom/components/link#special-rendering-behavior) React blocks the boundary until the stylesheet loads, up to a timeout. [See an example below.](#waiting-for-a-stylesheet-to-load)
4848
- Waiting for a large boundary's HTML to arrive during streaming server rendering. Sending HTML takes time, so a boundary with enough content activates even when nothing in it suspends. React reveals the content as the HTML arrives.
4949
- Loading fonts. This doesn't happen by default, but a [`<ViewTransition>`](/reference/react/ViewTransition) update waits for new fonts to load, up to a timeout, so text doesn't flash with a fallback font. [See an example below.](#waiting-for-a-font-to-load)
5050
- Loading images. This doesn't happen by default, but a [`<ViewTransition>`](/reference/react/ViewTransition) update waits for its images to load, up to a timeout. Adding an `onLoad` handler opts a specific image out. [See an example below.](#waiting-for-an-image-to-load)
@@ -2251,6 +2251,181 @@ main {
22512251
22522252
---
22532253
2254+
### Resetting Suspense boundaries on navigation {/*resetting-suspense-boundaries-on-navigation*/}
2255+
2256+
During a Transition, React avoids hiding already revealed content. However, when you navigate to *different* content, such as another user's profile, you'll want the boundary to show the fallback instead of the previous content. You can express this with a `key`:
2257+
2258+
```js
2259+
<ProfilePage key={queryParams.id} />
2260+
```
2261+
2262+
With a different `key`, React treats the profiles as different components and resets the Suspense boundary during navigation. Suspense-integrated routers do this automatically.
2263+
2264+
In the example below, the `key` resets the boundary when you switch profiles, so the fallback shows instead of the previous user's bio. Try removing the `key`: the previous bio stays visible while the next one loads:
2265+
2266+
<Sandpack>
2267+
2268+
```js
2269+
import { Suspense, useState, startTransition } from 'react';
2270+
import Bio from './Bio.js';
2271+
import { fetchBio } from './data.js';
2272+
2273+
export default function App() {
2274+
const [user, setUser] = useState(() => ({
2275+
id: 'alice',
2276+
bioPromise: fetchBio('alice'),
2277+
}));
2278+
function navigate(id) {
2279+
startTransition(() => {
2280+
setUser({ id, bioPromise: fetchBio(id) });
2281+
});
2282+
}
2283+
return (
2284+
<>
2285+
<button onClick={() => navigate('alice')}>
2286+
Alice
2287+
</button>
2288+
<button onClick={() => navigate('bob')}>
2289+
Bob
2290+
</button>
2291+
<Suspense key={user.id} fallback={<p>⌛ Loading profile...</p>}>
2292+
<Bio bioPromise={user.bioPromise} />
2293+
</Suspense>
2294+
</>
2295+
);
2296+
}
2297+
```
2298+
2299+
```js src/Bio.js
2300+
import { use } from 'react';
2301+
2302+
export default function Bio({ bioPromise }) {
2303+
const bio = use(bioPromise);
2304+
return <p>{bio}</p>;
2305+
}
2306+
```
2307+
2308+
```js src/data.js hidden
2309+
// Note: the way you would do data fetching depends on
2310+
// the framework that you use together with Suspense.
2311+
// Normally, the caching logic would be inside a framework.
2312+
2313+
export async function fetchBio(userId) {
2314+
// Add a fake delay to make waiting noticeable.
2315+
await new Promise(resolve => {
2316+
setTimeout(resolve, 1500);
2317+
});
2318+
2319+
return userId === 'alice'
2320+
? 'Alice is a photographer and traveler.'
2321+
: 'Bob collects vintage synthesizers.';
2322+
}
2323+
```
2324+
2325+
```css
2326+
button {
2327+
margin-right: 8px;
2328+
}
2329+
```
2330+
2331+
</Sandpack>
2332+
2333+
---
2334+
2335+
### Providing a fallback for server errors and client-only content {/*providing-a-fallback-for-server-errors-and-client-only-content*/}
2336+
2337+
If you use one of the [streaming server rendering APIs](/reference/react-dom/server) (or a framework that relies on them), React will also use your `<Suspense>` boundaries to handle errors on the server. If a component throws an error on the server, React will not abort the server render. Instead, it will find the closest `<Suspense>` component above it and include its fallback (such as a spinner) into the generated server HTML. The user will see a spinner at first.
2338+
2339+
On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest [Error Boundary.](/reference/react/Component#static-getderivedstatefromerror) However, if it does not error on the client, React will not display the error to the user since the content was eventually displayed successfully.
2340+
2341+
You can use this to opt out some components from rendering on the server. To do this, throw an error in the server environment and then wrap them in a `<Suspense>` boundary to replace their HTML with fallbacks:
2342+
2343+
```js
2344+
<Suspense fallback={<Loading />}>
2345+
<Chat />
2346+
</Suspense>
2347+
2348+
function Chat() {
2349+
if (typeof window === 'undefined') {
2350+
throw Error('Chat should only render on the client.');
2351+
}
2352+
// ...
2353+
}
2354+
```
2355+
2356+
The server HTML will include the loading indicator. It will be replaced by the `Chat` component on the client.
2357+
2358+
---
2359+
2360+
### Waiting for a stylesheet to load {/*waiting-for-a-stylesheet-to-load*/}
2361+
2362+
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 never appears unstyled.
2363+
2364+
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:
2365+
2366+
<Sandpack>
2367+
2368+
```js
2369+
import { Suspense, useState, startTransition } from 'react';
2370+
2371+
const stylesheet =
2372+
'https://fonts.googleapis.com/css2?family=Caveat&display=block';
2373+
2374+
function Card({ href }) {
2375+
return (
2376+
<>
2377+
<link rel="stylesheet" href={href} precedence="default" />
2378+
<div className="fancy-card">This card is styled by the stylesheet.</div>
2379+
</>
2380+
);
2381+
}
2382+
2383+
export default function App() {
2384+
const [href, setHref] = useState(null);
2385+
return (
2386+
<>
2387+
<button
2388+
onClick={() => {
2389+
startTransition(() => {
2390+
// Add a unique parameter so the stylesheet isn't
2391+
// cached, and every run shows the loading state.
2392+
setHref(stylesheet + '&t=' + Date.now());
2393+
});
2394+
}}>
2395+
Show card
2396+
</button>
2397+
{href && (
2398+
<Suspense fallback={<p>⌛ Loading styles...</p>}>
2399+
<Card href={href} />
2400+
</Suspense>
2401+
)}
2402+
</>
2403+
);
2404+
}
2405+
```
2406+
2407+
```css
2408+
#root {
2409+
min-height: 140px;
2410+
}
2411+
button {
2412+
margin-right: 8px;
2413+
}
2414+
.fancy-card {
2415+
margin-top: 1em;
2416+
padding: 20px;
2417+
border-radius: 8px;
2418+
color: white;
2419+
font-family: 'Caveat', cursive;
2420+
font-size: 24px;
2421+
background: linear-gradient(135deg, #087ea4, #2b3491);
2422+
}
2423+
```
2424+
2425+
</Sandpack>
2426+
2427+
---
2428+
22542429
### Animating from Suspense content {/*animating-from-suspense-content*/}
22552430
22562431
<CanaryBadge /> Suspense composes with [`<ViewTransition>`](/reference/react/ViewTransition) to animate the swap from the fallback to the content. Wrap the boundary in a `<ViewTransition>`, and React treats the swap as an update, cross-fading between the fallback and the content by default:
@@ -2493,17 +2668,19 @@ Where you place the `<ViewTransition>` relative to the boundary determines wheth
24932668
24942669
<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 doesn't happen by default outside a `<ViewTransition>`.
24952670
2496-
In the example below, the `Quote` component suspends while its data loads. The browser only starts downloading a font when text first uses it, so rendering the quote starts the font download. React holds the animated reveal until the font has loaded:
2671+
In the example below, the quote uses a new font. The browser only starts downloading a font when text first uses it, so rendering the quote starts the download. React holds the animated reveal until the font has loaded:
24972672
24982673
<Sandpack>
24992674
25002675
```js
2501-
import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
2502-
import { fetchQuote } from './data.js';
2676+
import { ViewTransition, Suspense, useState, startTransition } from 'react';
25032677

25042678
function Quote() {
2505-
const quote = use(fetchQuote());
2506-
return <p className="quote fancy">{quote}</p>;
2679+
return (
2680+
<p className="quote fancy">
2681+
The best way to predict the future is to invent it.
2682+
</p>
2683+
);
25072684
}
25082685

25092686
export default function App() {
@@ -2530,28 +2707,6 @@ export default function App() {
25302707
}
25312708
```
25322709
2533-
```js src/data.js hidden
2534-
// Note: the way you would do data fetching depends on
2535-
// the framework that you use together with Suspense.
2536-
// Normally, the caching logic would be inside a framework.
2537-
2538-
let cache = null;
2539-
2540-
export function fetchQuote() {
2541-
if (!cache) {
2542-
cache = new Promise((resolve) => {
2543-
// Add a fake delay to make waiting noticeable.
2544-
setTimeout(() => {
2545-
resolve(
2546-
'The best way to predict the future is to invent it.'
2547-
);
2548-
}, 1500);
2549-
});
2550-
}
2551-
return cache;
2552-
}
2553-
```
2554-
25552710
```css
25562711
/* The browser doesn't download the font until
25572712
text first renders with this font family. */
@@ -2590,25 +2745,23 @@ export function fetchQuote() {
25902745
25912746
<CanaryBadge /> Images work the same way: when a [`<ViewTransition>`](/reference/react/ViewTransition) animates a boundary's reveal, React waits for visible images in the content to load before revealing the content, so the animation doesn't finish on a half-loaded image. This doesn't happen by default outside a `<ViewTransition>`. Adding an `onLoad` handler opts a specific image out, even inside a `<ViewTransition>`.
25922747
2593-
In the example below, the `Scientist` component suspends while its data loads. When the data is ready, React holds the animated reveal until the portrait has loaded:
2748+
In the example below, React holds the animated reveal until the portrait has loaded:
25942749
25952750
<Sandpack>
25962751
25972752
```js
2598-
import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
2599-
import { fetchScientist } from './data.js';
2753+
import { ViewTransition, Suspense, useState, startTransition } from 'react';
26002754

26012755
function Scientist() {
2602-
const scientist = use(fetchScientist());
26032756
return (
26042757
<div className="card">
26052758
<img
2606-
src={scientist.imageUrl}
2607-
alt={scientist.name}
2759+
src="https://react.dev/images/docs/scientists/MK3eW3Am.jpg"
2760+
alt="Katherine Johnson"
26082761
width={100}
26092762
height={100}
26102763
/>
2611-
<p>{scientist.name}</p>
2764+
<p>Katherine Johnson</p>
26122765
</div>
26132766
);
26142767
}
@@ -2637,29 +2790,6 @@ export default function App() {
26372790
}
26382791
```
26392792
2640-
```js src/data.js hidden
2641-
// Note: the way you would do data fetching depends on
2642-
// the framework that you use together with Suspense.
2643-
// Normally, the caching logic would be inside a framework.
2644-
2645-
let cache = null;
2646-
2647-
export function fetchScientist() {
2648-
if (!cache) {
2649-
cache = new Promise((resolve) => {
2650-
// Add a fake delay to make waiting noticeable.
2651-
setTimeout(() => {
2652-
resolve({
2653-
name: 'Katherine Johnson',
2654-
imageUrl: 'https://react.dev/images/docs/scientists/MK3eW3Am.jpg',
2655-
});
2656-
}, 1500);
2657-
});
2658-
}
2659-
return cache;
2660-
}
2661-
```
2662-
26632793
```css
26642794
#root {
26652795
min-height: 220px;
@@ -2689,45 +2819,6 @@ export function fetchScientist() {
26892819
26902820
---
26912821
2692-
### Resetting Suspense boundaries on navigation {/*resetting-suspense-boundaries-on-navigation*/}
2693-
2694-
During a Transition, React will avoid hiding already revealed content. However, if you navigate to a route with different parameters, you might want to tell React it is *different* content. You can express this with a `key`:
2695-
2696-
```js
2697-
<ProfilePage key={queryParams.id} />
2698-
```
2699-
2700-
Imagine you're navigating within a user's profile page, and something suspends. If that update is wrapped in a Transition, it will not trigger the fallback for already visible content. That's the expected behavior.
2701-
2702-
However, now imagine you're navigating between two different user profiles. In that case, it makes sense to show the fallback. For example, one user's timeline is *different content* from another user's timeline. By specifying a `key`, you ensure that React treats different users' profiles as different components, and resets the Suspense boundaries during navigation. Suspense-integrated routers should do this automatically.
2703-
2704-
---
2705-
2706-
### Providing a fallback for server errors and client-only content {/*providing-a-fallback-for-server-errors-and-client-only-content*/}
2707-
2708-
If you use one of the [streaming server rendering APIs](/reference/react-dom/server) (or a framework that relies on them), React will also use your `<Suspense>` boundaries to handle errors on the server. If a component throws an error on the server, React will not abort the server render. Instead, it will find the closest `<Suspense>` component above it and include its fallback (such as a spinner) into the generated server HTML. The user will see a spinner at first.
2709-
2710-
On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest [Error Boundary.](/reference/react/Component#static-getderivedstatefromerror) However, if it does not error on the client, React will not display the error to the user since the content was eventually displayed successfully.
2711-
2712-
You can use this to opt out some components from rendering on the server. To do this, throw an error in the server environment and then wrap them in a `<Suspense>` boundary to replace their HTML with fallbacks:
2713-
2714-
```js
2715-
<Suspense fallback={<Loading />}>
2716-
<Chat />
2717-
</Suspense>
2718-
2719-
function Chat() {
2720-
if (typeof window === 'undefined') {
2721-
throw Error('Chat should only render on the client.');
2722-
}
2723-
// ...
2724-
}
2725-
```
2726-
2727-
The server HTML will include the loading indicator. It will be replaced by the `Chat` component on the client.
2728-
2729-
---
2730-
27312822
## Troubleshooting {/*troubleshooting*/}
27322823
27332824
### How do I prevent the UI from being replaced by a fallback during an update? {/*preventing-unwanted-fallbacks*/}

0 commit comments

Comments
 (0)