Skip to content

Commit d2df82a

Browse files
committed
Address feedback
1 parent 201b5e3 commit d2df82a

1 file changed

Lines changed: 78 additions & 51 deletions

File tree

src/content/reference/react/Suspense.md

Lines changed: 78 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ A Suspense boundary waits for its content to be ready before revealing it. Any o
214214
- Lazy-loading component code with [`lazy`](/reference/react/lazy).
215215
- Reading a Promise with [`use`](/reference/react/use), including data streamed from [Server Components](/reference/rsc/server-components) and integrations from frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/).
216216
- 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.
217-
- Loading fonts. React blocks a streamed boundary until [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) resolves, up to a timeout. Fonts also block a [`<ViewTransition>`](/reference/react/ViewTransition) update.
217+
- Loading fonts. When a boundary is revealed by streamed SSR content, React waits for [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) before showing it, up to a timeout, so text doesn't flash with a fallback font. Fonts also block a [`<ViewTransition>`](/reference/react/ViewTransition) update.
218218
- Streaming a large boundary's HTML during server rendering. React [reveals the content as the HTML arrives.](/reference/react-dom/server/renderToReadableStream#streaming-more-content-as-it-loads)
219219
- Loading an image, where the `src` blocks the boundary until the image loads. This behavior is not enabled by default. When enabled, an `onLoad` handler opts an image out, and images in a [`<ViewTransition>`](/reference/react/ViewTransition) update opt in automatically.
220220
- <ExperimentalBadge /> Performing CPU-bound render work inside a `<Suspense>` boundary marked with the `defer` prop.
@@ -229,60 +229,28 @@ Without a framework, you can read a Promise with `use` directly, as long as the
229229

230230
</Note>
231231

232-
For example, both boundaries below are set up identically. The one on the left activates because its content reads a Promise with `use`, so it shows the `fallback` while loading. The one on the right fetches the same data inside an Effect, which Suspense can't detect, so its `fallback` never appears and the albums simply show up once the fetch resolves:
232+
Fetching data inside an Effect does not activate the boundary. Suspense can't detect the fetch, so the `fallback` never appears and the list stays empty until the data arrives:
233233

234234
<Sandpack>
235235

236236
```js
237237
import { Suspense } from 'react';
238-
import Albums from './Albums.js';
239238
import EffectAlbums from './EffectAlbums.js';
240239

241240
export default function App() {
242241
return (
243-
<div className="panels">
244-
<section className="panel">
245-
<h2>Reads a Promise with use</h2>
246-
<Suspense fallback={<Loading />}>
247-
<Albums artistId="the-beatles" />
248-
</Suspense>
249-
</section>
250-
<section className="panel">
251-
<h2>Fetches in an Effect</h2>
252-
<Suspense fallback={<Loading />}>
253-
<EffectAlbums artistId="the-beatles" />
254-
</Suspense>
255-
</section>
256-
</div>
242+
<Suspense fallback={<Loading />}>
243+
<EffectAlbums artistId="the-beatles" />
244+
</Suspense>
257245
);
258246
}
259247

260248
function Loading() {
261-
return <p>🌀 Loading...</p>;
262-
}
263-
```
264-
265-
```js src/Albums.js active
266-
import { use } from 'react';
267-
import { fetchData } from './data.js';
268-
269-
export default function Albums({ artistId }) {
270-
// Reading the Promise with `use` activates
271-
// the Suspense boundary while it loads.
272-
const albums = use(fetchData(`/${artistId}/albums`));
273-
return (
274-
<ul>
275-
{albums.map(album => (
276-
<li key={album.id}>
277-
{album.title} ({album.year})
278-
</li>
279-
))}
280-
</ul>
281-
);
249+
return <h2>🌀 Loading...</h2>;
282250
}
283251
```
284252

285-
```js src/EffectAlbums.js
253+
```js src/EffectAlbums.js active
286254
import { useState, useEffect } from 'react';
287255
import { fetchData } from './data.js';
288256

@@ -399,22 +367,81 @@ async function getAlbums() {
399367
}
400368
```
401369

402-
```css
403-
.panels {
404-
display: flex;
405-
gap: 20px;
370+
</Sandpack>
371+
372+
During streaming server rendering, a boundary also activates as its HTML arrives. With any streaming SSR API, React sends the shell with the `fallback` first, then streams in each boundary's HTML and swaps out its `fallback` as that content arrives. This progressive reveal applies only to content streamed from the server, not to updates on the client:
373+
374+
<Sandpack>
375+
376+
```js src/App.js hidden
377+
```
378+
379+
```html public/index.html
380+
<!DOCTYPE html>
381+
<html lang="en">
382+
<head>
383+
<meta charset="UTF-8" />
384+
<title>Streaming SSR</title>
385+
</head>
386+
<body>
387+
<iframe id="container" style="width: 100%; height: 180px; border: 1px solid #aaa;"></iframe>
388+
</body>
389+
</html>
390+
```
391+
392+
```js src/index.js
393+
import { flushReadableStreamToFrame } from './demo-helpers.js';
394+
import { Suspense, use } from 'react';
395+
import { renderToReadableStream } from 'react-dom/server';
396+
397+
const { promise: posts, resolve: resolvePosts } =
398+
Promise.withResolvers();
399+
400+
function Posts() {
401+
const text = use(posts);
402+
return <p>{text}</p>;
406403
}
407404

408-
.panel {
409-
flex: 1;
410-
border: 1px solid #aaa;
411-
border-radius: 6px;
412-
padding: 10px;
405+
function ProfilePage() {
406+
return (
407+
<html>
408+
<body>
409+
<h1>Alice</h1>
410+
<p>Photographer and traveler.</p>
411+
<Suspense fallback={<p>Loading posts...</p>}>
412+
<Posts />
413+
</Suspense>
414+
</body>
415+
</html>
416+
);
413417
}
414418

415-
.panel h2 {
416-
margin-top: 0;
417-
font-size: 1rem;
419+
async function main(frame) {
420+
const stream = await renderToReadableStream(<ProfilePage />);
421+
422+
// The posts resolve after the shell has streamed, so React
423+
// streams their HTML in and swaps out the fallback.
424+
setTimeout(() => {
425+
resolvePosts(
426+
'Just got back from two weeks along the coast. The drive ' +
427+
'was longer than expected, but every stop was worth it. ' +
428+
'A full write-up and more photos are coming soon.'
429+
);
430+
}, 1500);
431+
432+
await flushReadableStreamToFrame(stream, frame);
433+
}
434+
435+
main(document.getElementById('container'));
436+
```
437+
438+
```js src/demo-helpers.js hidden
439+
export async function flushReadableStreamToFrame(readable, frame) {
440+
const doc = frame.contentWindow.document;
441+
const decoder = new TextDecoder();
442+
for await (const chunk of readable) {
443+
doc.write(decoder.decode(chunk));
444+
}
418445
}
419446
```
420447

0 commit comments

Comments
 (0)