diff --git a/src/content/reference/react-dom/server/renderToPipeableStream.md b/src/content/reference/react-dom/server/renderToPipeableStream.md
index 84b8873a6fd..520d7e6a30c 100644
--- a/src/content/reference/react-dom/server/renderToPipeableStream.md
+++ b/src/content/reference/react-dom/server/renderToPipeableStream.md
@@ -284,17 +284,7 @@ Streaming does not need to wait for React itself to load in the browser, or for
-**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.
diff --git a/src/content/reference/react-dom/server/renderToReadableStream.md b/src/content/reference/react-dom/server/renderToReadableStream.md
index f3e862124af..d90a03f2bae 100644
--- a/src/content/reference/react-dom/server/renderToReadableStream.md
+++ b/src/content/reference/react-dom/server/renderToReadableStream.md
@@ -283,17 +283,7 @@ Streaming does not need to wait for React itself to load in the browser, or for
-**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.
diff --git a/src/content/reference/react-dom/static/prerender.md b/src/content/reference/react-dom/static/prerender.md
index 8ad47aa15f3..7f241c4926d 100644
--- a/src/content/reference/react-dom/static/prerender.md
+++ b/src/content/reference/react-dom/static/prerender.md
@@ -275,17 +275,7 @@ Imagine that `` needs to load some data, which takes some time. Ideally
-**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.
diff --git a/src/content/reference/react-dom/static/prerenderToNodeStream.md b/src/content/reference/react-dom/static/prerenderToNodeStream.md
index 7a31f66a1e4..4bef38e3831 100644
--- a/src/content/reference/react-dom/static/prerenderToNodeStream.md
+++ b/src/content/reference/react-dom/static/prerenderToNodeStream.md
@@ -276,17 +276,7 @@ Imagine that `` needs to load some data, which takes some time. Ideally
-**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.
diff --git a/src/content/reference/react/Activity.md b/src/content/reference/react/Activity.md
index 127a4b8d0ae..b521970b764 100644
--- a/src/content/reference/react/Activity.md
+++ b/src/content/reference/react/Activity.md
@@ -755,17 +755,7 @@ Pre-rendering components with hidden Activity boundaries is a powerful way to re
-**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.
diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md
index c2fc0b6ef55..3439da58ed7 100644
--- a/src/content/reference/react/Suspense.md
+++ b/src/content/reference/react/Suspense.md
@@ -29,13 +29,39 @@ title:
#### 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.
---
+### What activates a Suspense boundary {/*what-activates-a-suspense-boundary*/}
+
+A Suspense boundary waits for its content to be ready before revealing it. Any of the following keeps a boundary from revealing its content:
+
+- 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) or loaded through a [Suspense-enabled framework](#suspense-enabled-frameworks).
+- Loading a stylesheet rendered with [`` 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)
+- 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.
+- Loading fonts. Suspense doesn't wait for fonts by default, but a [``](/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)
+- Loading images. Suspense doesn't wait for images by default, but during a [``](/reference/react/ViewTransition) update, React blocks the boundary until the image loads, up to a timeout. Adding an `onLoad` handler opts a specific image out. [See an example below.](#waiting-for-an-image-to-load)
+- Performing CPU-bound render work inside a `` boundary marked with the `defer` prop.
+
+
+
+#### Suspense-enabled frameworks {/*suspense-enabled-frameworks*/}
+
+A *Suspense-enabled framework* gives you a way to read data in your component in a way that activates the closest Suspense boundary. The exact way you load your data depends on your framework, and you'll find the details in its documentation. Under the hood, a Suspense-enabled framework maintains a cache of Promises and calls [`use`](/reference/react/use) to suspend on a Promise.
+
+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)
+
+
+
+---
+
## Usage {/*usage*/}
### Displaying a fallback while content is loading {/*displaying-a-fallback-while-content-is-loading*/}
@@ -203,21 +229,256 @@ async function getAlbums() {
-
+By contrast, code that fetches data outside of `use`, such as inside an Effect, does not activate the boundary:
-**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/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)
+```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 (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+}
+```
-Suspense **does not** detect when data is fetched inside an Effect or event handler.
+```js src/ArtistPage.js active
+import { Suspense } from 'react';
+import EffectAlbums from './EffectAlbums.js';
-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.
+export default function ArtistPage({ artist }) {
+ return (
+ <>
+
{artist.name}
+ }>
+
+
+ >
+ );
+}
-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.
+function Loading() {
+ return
🌀 Loading...
;
+}
+```
-
+```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 (
+
+ {albums.map(album => (
+
+ {album.title} ({album.year})
+
+ ))}
+
+ );
+}
+```
+
+```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
+ }];
+}
+```
+
+
+
+During streaming server rendering, a boundary also activates while its HTML is still streaming in. With any streaming server rendering API, React sends [the shell](/reference/react-dom/server/renderToPipeableStream#specifying-what-goes-into-the-shell) with the `fallback` first, then streams in each boundary's HTML and swaps out its `fallback` as that content arrives. Press "Render the page" to watch the page stream in:
+
+
+
+```js src/App.js hidden
+```
+
+```html public/index.html
+
+
+
+
+ Streaming SSR
+
+
+
+
+
+
+
+```
+
+```js src/index.js
+import { flushReadableStreamToFrame } from './demo-helpers.js';
+import { Suspense, use } from 'react';
+import { renderToReadableStream } from 'react-dom/server';
+
+let posts = null;
+
+function Posts() {
+ const text = use(posts.promise);
+ return
{text}
;
+}
+
+function ProfilePage() {
+ return (
+
+
+
Alice
+
Photographer and traveler.
+ ⌛ Loading posts...
}>
+
+
+
+
+ );
+}
+
+async function main(frame) {
+ posts = Promise.withResolvers();
+ const stream = await renderToReadableStream();
+
+ // The posts resolve after the shell has streamed, so React
+ // streams their HTML in and swaps out the fallback.
+ setTimeout(() => {
+ posts.resolve(
+ '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);
+}
+
+document.getElementById('render').addEventListener('click', () => {
+ 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, { stream: true }));
+ }
+ doc.close();
+}
+```
+
+
---
@@ -1992,15 +2253,98 @@ main {
### Resetting Suspense boundaries on navigation {/*resetting-suspense-boundaries-on-navigation*/}
-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`:
+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`:
```js
```
-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.
+With a different `key`, React treats the profiles as different content and resets the Suspense boundary during navigation. The `key` can go on the boundary itself or on a component above it. Suspense-integrated routers should do this automatically.
+
+In the example below, opening the profile page loads the first profile. Pressing "Bob" navigates to a different profile, and the `key` resets the boundary, 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:
+
+
+
+```js src/App.js hidden
+import { useState } from 'react';
+import ProfilePage from './ProfilePage.js';
+
+export default function App() {
+ const [show, setShow] = useState(false);
+ if (show) {
+ return ;
+ }
+ return (
+
+ );
+}
+```
+
+```js src/ProfilePage.js active
+import { Suspense, useState, startTransition } from 'react';
+import Bio from './Bio.js';
+import { fetchBio } from './data.js';
+
+export default function ProfilePage() {
+ const [user, setUser] = useState(() => ({
+ id: 'alice',
+ bioPromise: fetchBio('alice'),
+ }));
+ function navigate(id) {
+ startTransition(() => {
+ setUser({ id, bioPromise: fetchBio(id) });
+ });
+ }
+ return (
+ <>
+
+
+ ⌛ Loading profile...}>
+
+
+ >
+ );
+}
+```
+
+```js src/Bio.js
+import { use } from 'react';
+
+export default function Bio({ bioPromise }) {
+ const bio = use(bioPromise);
+ return
{bio}
;
+}
+```
+
+```js src/data.js hidden
+// Note: the way you would do data fetching depends on
+// the framework that you use together with Suspense.
-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.
+export async function fetchBio(userId) {
+ // Add a fake delay to make waiting noticeable.
+ await new Promise(resolve => {
+ setTimeout(resolve, 1500);
+ });
+
+ return userId === 'alice'
+ ? 'Alice is a photographer and traveler.'
+ : 'Bob collects vintage synthesizers.';
+}
+```
+
+```css
+button {
+ margin-right: 8px;
+}
+```
+
+
---
@@ -2029,6 +2373,495 @@ The server HTML will include the loading indicator. It will be replaced by the `
---
+### Waiting for a stylesheet to load {/*waiting-for-a-stylesheet-to-load*/}
+
+A stylesheet rendered with [`` 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.
+
+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:
+
+
+
+```js
+import { Suspense, useState, startTransition } from 'react';
+
+const stylesheet =
+ 'https://fonts.googleapis.com/css2?family=Caveat&display=block';
+
+function Card({ href }) {
+ return (
+ <>
+
+
This card uses a font from the stylesheet.
+ >
+ );
+}
+
+export default function App() {
+ const [href, setHref] = useState(null);
+ return (
+ <>
+
+ {href && (
+ ⌛ Loading styles...}>
+
+
+ )}
+ >
+ );
+}
+```
+
+```css
+#root {
+ min-height: 140px;
+}
+button {
+ margin-right: 8px;
+}
+.fancy-card {
+ margin-top: 1em;
+ padding: 20px;
+ border-radius: 8px;
+ color: white;
+ font-family: 'Caveat', cursive;
+ font-size: 24px;
+ background: linear-gradient(135deg, #087ea4, #2b3491);
+}
+```
+
+
+
+---
+
+### Animating from Suspense content {/*animating-from-suspense-content*/}
+
+ Suspense composes with [``](/reference/react/ViewTransition) to animate the swap from the fallback to the content. Wrap the boundary in a ``, and React treats the swap as an update, cross-fading between the fallback and the content by default:
+
+
+
+```js src/Video.js hidden
+function Thumbnail({video, children}) {
+ return (
+
+ );
+}
+
+export function Video({video}) {
+ return (
+
+
+
+
+
{video.title}
+
{video.description}
+
+
+
+ );
+}
+
+export function VideoPlaceholder() {
+ const video = {image: 'loading'};
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+```js
+import {ViewTransition, useState, startTransition, Suspense} from 'react';
+import {Video, VideoPlaceholder} from './Video';
+import {useLazyVideoData} from './data';
+
+function LazyVideo() {
+ const video = useLazyVideoData();
+ return ;
+}
+
+export default function Component() {
+ const [showItem, setShowItem] = useState(false);
+ return (
+ <>
+
+ {showItem ? (
+
+ }>
+
+
+
+ ) : null}
+ >
+ );
+}
+```
+
+```js src/data.js hidden
+import {use} from 'react';
+
+let cache = null;
+
+function fetchVideo() {
+ if (!cache) {
+ cache = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ id: '1',
+ title: 'First video',
+ description: 'Video description',
+ image: 'blue',
+ });
+ }, 1000);
+ });
+ }
+ return cache;
+}
+
+export function useLazyVideoData() {
+ return use(fetchVideo());
+}
+```
+
+```css
+#root {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-height: 200px;
+}
+button {
+ border: none;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #f0f8ff;
+ color: white;
+ font-size: 20px;
+ cursor: pointer;
+ transition: background-color 0.3s, border 0.3s;
+}
+button:hover {
+ border: 2px solid #ccc;
+ background-color: #e0e8ff;
+}
+.thumbnail {
+ position: relative;
+ aspect-ratio: 16 / 9;
+ display: flex;
+ overflow: hidden;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ border-radius: 0.5rem;
+ outline-offset: 2px;
+ width: 8rem;
+ vertical-align: middle;
+ background-color: #ffffff;
+ background-size: cover;
+ user-select: none;
+}
+.thumbnail.blue {
+ background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);
+}
+.loading {
+ background-image: linear-gradient(
+ 90deg,
+ rgba(173, 216, 230, 0.3) 25%,
+ rgba(135, 206, 250, 0.5) 50%,
+ rgba(173, 216, 230, 0.3) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+ 100% {
+ background-position: 200% 0;
+ }
+}
+.video {
+ display: flex;
+ flex-direction: row;
+ gap: 0.75rem;
+ align-items: center;
+ margin-top: 1em;
+}
+.video .link {
+ display: flex;
+ flex-direction: row;
+ flex: 1 1 0;
+ gap: 0.125rem;
+ outline-offset: 4px;
+ cursor: pointer;
+}
+.video .info {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-left: 8px;
+ gap: 0.125rem;
+}
+.video .info:hover {
+ text-decoration: underline;
+}
+.video-title {
+ font-size: 15px;
+ line-height: 1.25;
+ font-weight: 700;
+ color: #23272f;
+}
+.video-title.loading {
+ height: 20px;
+ width: 80px;
+ border-radius: 0.5rem;
+}
+.video-description {
+ color: #5e687e;
+ font-size: 13px;
+ border-radius: 0.5rem;
+}
+.video-description.loading {
+ height: 15px;
+ width: 100px;
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "canary",
+ "react-dom": "canary",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+
+
+Where you place the `` relative to the boundary determines whether the fallback and content cross-fade as one update or animate as separate exit and enter animations. You can also [customize the animation](/reference/react/ViewTransition#customizing-animations) with View Transition classes.
+
+[Learn more about animating from Suspense content.](/reference/react/ViewTransition#animating-from-suspense-content)
+
+
+
+---
+
+### Waiting for a font to load {/*waiting-for-a-font-to-load*/}
+
+ When a [``](/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 `` update.
+
+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:
+
+
+
+```js
+import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
+import { fetchQuote } from './data.js';
+
+const fontUrl =
+ 'https://raw.githubusercontent.com/google/fonts/main/ofl/caveat/Caveat%5Bwght%5D.ttf';
+
+function Quote({ fontSrc }) {
+ const quote = use(fetchQuote());
+ return (
+ <>
+
+
{quote}
+ >
+ );
+}
+
+export default function App() {
+ const [fontSrc, setFontSrc] = useState(null);
+ return (
+ <>
+
+ {fontSrc && (
+
+ ⌛ Loading quote...}>
+
+
+
+ )}
+ >
+ );
+}
+```
+
+```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 = null;
+
+export function fetchQuote() {
+ if (!cache) {
+ cache = new Promise((resolve) => {
+ // Add a fake delay to make waiting noticeable.
+ setTimeout(() => {
+ resolve(
+ 'The best way to predict the future is to invent it.'
+ );
+ }, 1500);
+ });
+ }
+ return cache;
+}
+```
+
+```css
+#root {
+ min-height: 100px;
+}
+.quote {
+ font-size: 20px;
+ margin-top: 1em;
+}
+.fancy {
+ font-family: 'Fancy', cursive;
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "canary",
+ "react-dom": "canary",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+---
+
+### Waiting for an image to load {/*waiting-for-an-image-to-load*/}
+
+ Images work the same way: when a [``](/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 `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``.
+
+In the example below, the boundary shows its fallback until the portrait has loaded:
+
+
+
+```js
+import { ViewTransition, Suspense, useState, startTransition } from 'react';
+
+const imageUrl = 'https://react.dev/images/docs/scientists/MK3eW3Am.jpg';
+
+function Scientist({ src }) {
+ return (
+
+
+
Katherine Johnson
+
+ );
+}
+
+export default function App() {
+ const [src, setSrc] = useState(null);
+ return (
+ <>
+
+ {src && (
+
+ ⌛ Loading image...}>
+
+
+
+ )}
+ >
+ );
+}
+```
+
+```css
+#root {
+ min-height: 220px;
+}
+.card {
+ margin-top: 1em;
+}
+.card img {
+ border-radius: 50%;
+}
+.card p {
+ font-weight: bold;
+}
+```
+
+```json package.json hidden
+{
+ "dependencies": {
+ "react": "canary",
+ "react-dom": "canary",
+ "react-scripts": "latest"
+ }
+}
+```
+
+
+
+---
+
## Troubleshooting {/*troubleshooting*/}
### How do I prevent the UI from being replaced by a fallback during an update? {/*preventing-unwanted-fallbacks*/}
diff --git a/src/content/reference/react/use.md b/src/content/reference/react/use.md
index 1780f82e7b5..611a0608aae 100644
--- a/src/content/reference/react/use.md
+++ b/src/content/reference/react/use.md
@@ -472,7 +472,7 @@ function Albums() {
}
```
-Instead, pass a Promise from a cache, a Suspense-enabled framework, or a Server Component:
+Instead, pass a Promise from a cache, a [Suspense-enabled framework](/reference/react/Suspense#suspense-enabled-frameworks), or a Server Component:
```js
// ✅ fetchData reads the Promise from a cache.
@@ -538,7 +538,7 @@ The `fetchData` function returns the same Promise each time it's called with the
-The way you cache Promises depends on the framework you use with Suspense. Frameworks typically provide built-in caching mechanisms. If you don't use a framework, you can use a simple module-level cache like the one above, or a [Suspense-enabled data source](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading).
+The way you cache Promises depends on the framework you use with Suspense. Frameworks typically provide built-in caching mechanisms. If you don't use a framework, you can use a simple module-level cache like the one above, or a [Suspense-enabled data source](/reference/react/Suspense#what-activates-a-suspense-boundary).
diff --git a/src/content/reference/react/useDeferredValue.md b/src/content/reference/react/useDeferredValue.md
index 40cb92629b5..bef15e1143a 100644
--- a/src/content/reference/react/useDeferredValue.md
+++ b/src/content/reference/react/useDeferredValue.md
@@ -86,11 +86,7 @@ During updates, the deferred value will "lag behin
-This example assumes you use a Suspense-enabled data source:
-
-- 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/getting-started/fetching-data#with-suspense)
-- Lazy-loading component code with [`lazy`](/reference/react/lazy)
-- Reading the value of a Promise with [`use`](/reference/react/use)
+This example assumes you use a data source that [activates a Suspense boundary](/reference/react/Suspense#what-activates-a-suspense-boundary), such as a Promise you read with [`use`](/reference/react/use).
[Learn more about Suspense and its limitations.](/reference/react/Suspense)