Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions src/content/reference/react-dom/server/renderToPipeableStream.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,17 +284,7 @@ Streaming does not need to wait for React itself to load in the browser, or for

<Note>

**Only Suspense-enabled data sources will activate the Suspense component.** They include:

- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/getting-started/react-essentials)
- Lazy-loading component code with [`lazy`](/reference/react/lazy)
- Reading the value of a Promise with [`use`](/reference/react/use)

Suspense **does not** detect when data is fetched inside an Effect or event handler.

The exact way you would load data in the `Posts` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Only data read from a source that [activates a Suspense boundary](/reference/react/Suspense#what-activates-a-suspense-boundary), such as a Promise read with [`use`](/reference/react/use), will suspend during rendering. Suspense does not detect data fetched inside an Effect or event handler.

</Note>

Expand Down
12 changes: 1 addition & 11 deletions src/content/reference/react-dom/server/renderToReadableStream.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,17 +283,7 @@ Streaming does not need to wait for React itself to load in the browser, or for

<Note>

**Only Suspense-enabled data sources will activate the Suspense component.** They include:

- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/getting-started/react-essentials)
- Lazy-loading component code with [`lazy`](/reference/react/lazy)
- Reading the value of a Promise with [`use`](/reference/react/use)

Suspense **does not** detect when data is fetched inside an Effect or event handler.

The exact way you would load data in the `Posts` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Only data read from a source that [activates a Suspense boundary](/reference/react/Suspense#what-activates-a-suspense-boundary), such as a Promise read with [`use`](/reference/react/use), will suspend during rendering. Suspense does not detect data fetched inside an Effect or event handler.

</Note>

Expand Down
12 changes: 1 addition & 11 deletions src/content/reference/react-dom/static/prerender.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,17 +275,7 @@ Imagine that `<Posts />` needs to load some data, which takes some time. Ideally

<Note>

**Only Suspense-enabled data sources will activate the Suspense component.** They include:

- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/getting-started/react-essentials)
- Lazy-loading component code with [`lazy`](/reference/react/lazy)
- Reading the value of a Promise with [`use`](/reference/react/use)

Suspense **does not** detect when data is fetched inside an Effect or event handler.

The exact way you would load data in the `Posts` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Only data read from a source that [activates a Suspense boundary](/reference/react/Suspense#what-activates-a-suspense-boundary), such as a Promise read with [`use`](/reference/react/use), will suspend during rendering. Suspense does not detect data fetched inside an Effect or event handler.

</Note>

Expand Down
12 changes: 1 addition & 11 deletions src/content/reference/react-dom/static/prerenderToNodeStream.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,17 +276,7 @@ Imagine that `<Posts />` needs to load some data, which takes some time. Ideally

<Note>

**Only Suspense-enabled data sources will activate the Suspense component.** They include:

- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/getting-started/react-essentials)
- Lazy-loading component code with [`lazy`](/reference/react/lazy)
- Reading the value of a Promise with [`use`](/reference/react/use)

Suspense **does not** detect when data is fetched inside an Effect or event handler.

The exact way you would load data in the `Posts` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Only data read from a source that [activates a Suspense boundary](/reference/react/Suspense#what-activates-a-suspense-boundary), such as a Promise read with [`use`](/reference/react/use), will suspend during rendering. Suspense does not detect data fetched inside an Effect or event handler.

</Note>

Expand Down
12 changes: 1 addition & 11 deletions src/content/reference/react/Activity.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,17 +755,7 @@ Pre-rendering components with hidden Activity boundaries is a powerful way to re

<Note>

**Only Suspense-enabled data sources will be fetched during pre-rendering.** They include:

- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense)
- Lazy-loading component code with [`lazy`](/reference/react/lazy)
- Reading the value of a cached Promise with [`use`](/reference/react/use)

Activity **does not** detect data that is fetched inside an Effect.

The exact way you would load data in the `Posts` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Only data read from a source that [activates a Suspense boundary](/reference/react/Suspense#what-activates-a-suspense-boundary), such as a Promise read with [`use`](/reference/react/use), is fetched during pre-rendering. Activity does not detect data fetched inside an Effect.

</Note>

Expand Down
273 changes: 265 additions & 8 deletions src/content/reference/react/Suspense.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ title: <Suspense>

#### Caveats {/*caveats*/}

- Suspense does not detect when data is fetched inside an Effect or event handler. It only activates in the [cases listed below.](#what-activates-a-suspense-boundary)
- React does not preserve any state for renders that got suspended before they were able to mount for the first time. When the component has loaded, React will retry rendering the suspended tree from scratch.
- If Suspense was displaying content for the tree, but then it suspended again, the `fallback` will be shown again unless the update causing it was caused by [`startTransition`](/reference/react/startTransition) or [`useDeferredValue`](/reference/react/useDeferredValue).
- React reveals suspended content at most once every 300ms, measured from the last reveal. Boundaries that become ready within that window are [revealed together](/blog/2025/10/01/react-19-2#batching-suspense-boundaries-for-ssr) rather than one at a time.
- If React needs to hide the already visible content because it suspended again, it will clean up [layout Effects](/reference/react/useLayoutEffect) in the content tree. When the content is ready to be shown again, React will fire the layout Effects again. This ensures that Effects measuring the DOM layout don't try to do this while the content is hidden.
- React includes under-the-hood optimizations like *Streaming Server Rendering* and *Selective Hydration* that are integrated with Suspense. Read [an architectural overview](https://github.com/reactwg/react-18/discussions/37) and watch [a technical talk](https://www.youtube.com/watch?v=pj5N-Khihgc) to learn more.

Expand Down Expand Up @@ -203,22 +205,277 @@ async function getAlbums() {

</Sandpack>

<Note>
---

### What activates a Suspense boundary {/*what-activates-a-suspense-boundary*/}

@MaxwellCohen MaxwellCohen Jun 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very excited to see these being called out and updated!

My understanding is that a "suspense enabled framework" is a framework that wraps these features that activates a suspense boundaries . If that is true could we specifically call it out what suspense enabled means?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this? 021f94d


**Only Suspense-enabled data sources will activate the Suspense component.** They include:
A Suspense boundary waits for its content to be ready before revealing it. Any of the following blocks a boundary's content from being revealed:

- Data fetching with Suspense-enabled frameworks like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) and [Next.js](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense)
- Lazy-loading component code with [`lazy`](/reference/react/lazy)
- Reading the value of a cached Promise with [`use`](/reference/react/use)
- Lazy-loading component code with [`lazy`](/reference/react/lazy).
- Reading a Promise with [`use`](/reference/react/use), including data streamed from [Server Components](/reference/rsc/server-components).
- Data fetching through a [Suspense-enabled framework](#suspense-enabled-frameworks) like [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/), which integrates its own data source with Suspense.
- 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.
- 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.
- Streaming a large boundary's HTML during server rendering. React reveals the content as the HTML arrives.
- 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.
- <ExperimentalBadge /> Performing CPU-bound render work inside a `<Suspense>` boundary marked with the `defer` prop.

Suspense **does not** detect when data is fetched inside an Effect or event handler.
<Note>

#### Suspense-enabled frameworks {/*suspense-enabled-frameworks*/}

The exact way you would load data in the `Albums` component above depends on your framework. If you use a Suspense-enabled framework, you'll find the details in its data fetching documentation.
A *Suspense-enabled framework* integrates its data fetching with Suspense, so that reading data in a component activates the nearest boundary. Some frameworks build on Server Components and [`use`](/reference/react/use), like Next.js, while others provide a custom integration, like Relay. The exact way you load your data depends on your framework, and you'll find the details in its documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.
Without a framework, you can read a Promise with `use` directly, as long as the Promise is [cached so the same instance is reused across renders.](/reference/react/use#caching-promises-for-client-components)

</Note>

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:

<Sandpack>

```js src/App.js hidden
import { useState } from 'react';
import ArtistPage from './ArtistPage.js';

export default function App() {
const [show, setShow] = useState(false);
if (show) {
return (
<ArtistPage
artist={{
id: 'the-beatles',
name: 'The Beatles',
}}
/>
);
} else {
return (
<button onClick={() => setShow(true)}>
Open The Beatles artist page
</button>
);
}
}
```

```js src/ArtistPage.js active
import { Suspense } from 'react';
import EffectAlbums from './EffectAlbums.js';

export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<EffectAlbums artistId={artist.id} />
</Suspense>
</>
);
}

function Loading() {
return <h2>🌀 Loading...</h2>;
}
```

```js src/EffectAlbums.js
import { useState, useEffect } from 'react';
import { fetchData } from './data.js';

export default function EffectAlbums({ artistId }) {
const [albums, setAlbums] = useState([]);

useEffect(() => {
let active = true;
fetchData(`/${artistId}/albums`).then(result => {
if (active) {
setAlbums(result);
}
});
return () => {
active = false;
};
}, [artistId]);

// Suspense can't see this fetch, so its fallback never
// shows. The list stays empty until the data arrives.
return (
<ul>
{albums.map(album => (
<li key={album.id}>
{album.title} ({album.year})
</li>
))}
</ul>
);
}
```

```js src/data.js hidden
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

let cache = new Map();

export function fetchData(url) {
if (!cache.has(url)) {
cache.set(url, getData(url));
}
return cache.get(url);
}

async function getData(url) {
if (url === '/the-beatles/albums') {
return await getAlbums();
} else {
throw Error('Not implemented');
}
}

async function getAlbums() {
// Add a fake delay to make waiting noticeable.
await new Promise(resolve => {
setTimeout(resolve, 3000);
});

return [{
id: 13,
title: 'Let It Be',
year: 1970
}, {
id: 12,
title: 'Abbey Road',
year: 1969
}, {
id: 11,
title: 'Yellow Submarine',
year: 1969
}, {
id: 10,
title: 'The Beatles',
year: 1968
}, {
id: 9,
title: 'Magical Mystery Tour',
year: 1967
}, {
id: 8,
title: 'Sgt. Pepper\'s Lonely Hearts Club Band',
year: 1967
}, {
id: 7,
title: 'Revolver',
year: 1966
}, {
id: 6,
title: 'Rubber Soul',
year: 1965
}, {
id: 5,
title: 'Help!',
year: 1965
}, {
id: 4,
title: 'Beatles For Sale',
year: 1964
}, {
id: 3,
title: 'A Hard Day\'s Night',
year: 1964
}, {
id: 2,
title: 'With The Beatles',
year: 1963
}, {
id: 1,
title: 'Please Please Me',
year: 1963
}];
}
```

</Sandpack>

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:

<Sandpack>

```js src/App.js hidden
```

```html public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Streaming SSR</title>
</head>
<body>
<iframe id="container" style="width: 100%; height: 180px; border: 1px solid #aaa;"></iframe>
</body>
</html>
```

```js src/index.js
import { flushReadableStreamToFrame } from './demo-helpers.js';
import { Suspense, use } from 'react';
import { renderToReadableStream } from 'react-dom/server';

const { promise: posts, resolve: resolvePosts } =
Promise.withResolvers();

function Posts() {
const text = use(posts);
return <p>{text}</p>;
}

function ProfilePage() {
return (
<html>
<body>
<h1>Alice</h1>
<p>Photographer and traveler.</p>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
</body>
</html>
);
}

async function main(frame) {
const stream = await renderToReadableStream(<ProfilePage />);

// The posts resolve after the shell has streamed, so React
// streams their HTML in and swaps out the fallback.
setTimeout(() => {
resolvePosts(
'Just got back from two weeks along the coast. The drive ' +
'was longer than expected, but every stop was worth it. ' +
'A full write-up and more photos are coming soon.'
);
}, 1500);

await flushReadableStreamToFrame(stream, frame);
}

main(document.getElementById('container'));
```

```js src/demo-helpers.js hidden
export async function flushReadableStreamToFrame(readable, frame) {
const doc = frame.contentWindow.document;
const decoder = new TextDecoder();
for await (const chunk of readable) {
doc.write(decoder.decode(chunk));
}
}
```

</Sandpack>

---

### Revealing content together at once {/*revealing-content-together-at-once*/}
Expand Down
Loading
Loading