From af129dbac298656dea4d67b537caafe68ba1c6f2 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 18:16:26 +0200 Subject: [PATCH 01/20] Clarify img View Transition example --- .../reference/react-dom/components/img.md | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index 4a1f45077b8..219bf8c66fb 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -129,16 +129,18 @@ During a client-rendered [``](/reference/react/ViewTransition) u When a Suspense boundary reveals streamed content inside a ``, React may also wait for visible images with a non-empty `src` that do not have `loading="lazy"`. React stops waiting after a timeout so that a slow image does not block the update indefinitely. -In this example, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. - -For comparison, the second button inserts the same card directly into the DOM. The card appears immediately, and the browser displays the image after it loads: +In this example, each button calls [`startTransition`](/reference/react/startTransition) to schedule its state update as a Transition. The first button renders the Suspense boundary inside a ``, so React shows a profile skeleton until the portrait has loaded. The second button renders the Suspense boundary outside a ``, so the card appears immediately and the browser displays the image after it loads: ```js -import { ViewTransition, Suspense, useState, startTransition } from 'react'; +import { + ViewTransition, + Suspense, + useState, + startTransition, +} from 'react'; import { freshImageUrl } from './image.js'; -import VanillaProfile from './VanillaProfile.js'; function Profile({ src }) { return ( @@ -159,47 +161,39 @@ function ProfilePlaceholder() { } export default function App() { - const [src, setSrc] = useState(null); + const [viewTransitionSrc, setViewTransitionSrc] = useState(null); + const [plainSrc, setPlainSrc] = useState(null); return ( <> - {src && ( + {viewTransitionSrc && ( }> - + )}
- - - ); -} -``` - -```js src/VanillaProfile.js -import { useRef } from 'react'; -import { freshImageUrl } from './image.js'; - -export default function VanillaProfile() { - const ref = useRef(null); - function show() { - ref.current.innerHTML = `
- Jack Pope -

Jack Pope

-
`; - } - return ( - <> - -
+ + {plainSrc && ( + }> + + + )} ); } From e638c50d2df86f2bcefae0d29ba9fea7c8efd4d7 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 18:20:37 +0200 Subject: [PATCH 02/20] Avoid redundant image alt text --- src/content/reference/react-dom/components/img.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index 219bf8c66fb..810dbdc37e2 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -145,7 +145,7 @@ import { freshImageUrl } from './image.js'; function Profile({ src }) { return (
- Jack Pope +

Jack Pope

); From f469cc2f651778538f02dbdee08443beb50c48c5 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 18:31:25 +0200 Subject: [PATCH 03/20] Tighten img example introduction --- src/content/reference/react-dom/components/img.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index 810dbdc37e2..ffbb0f0fdc4 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -129,7 +129,7 @@ During a client-rendered [``](/reference/react/ViewTransition) u When a Suspense boundary reveals streamed content inside a ``, React may also wait for visible images with a non-empty `src` that do not have `loading="lazy"`. React stops waiting after a timeout so that a slow image does not block the update indefinitely. -In this example, each button calls [`startTransition`](/reference/react/startTransition) to schedule its state update as a Transition. The first button renders the Suspense boundary inside a ``, so React shows a profile skeleton until the portrait has loaded. The second button renders the Suspense boundary outside a ``, so the card appears immediately and the browser displays the image after it loads: +In this example, [`startTransition`](/reference/react/startTransition) marks both state updates as Transitions. Compare what happens when React renders the image inside and outside a ``: From 4756b2d596a880e715a28342ecaf797b6b4fc5b4 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 18:36:07 +0200 Subject: [PATCH 04/20] Move img example outcome below demo --- src/content/reference/react-dom/components/img.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index ffbb0f0fdc4..b505c1e6f46 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -249,3 +249,5 @@ hr { ``` + +Inside ``, React keeps the skeleton visible while it waits for the image, up to a timeout. Outside it, React does not wait for the image before committing the card. From 4b668249c3874cf1a72ca3a1703f29623230d34c Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 18:36:55 +0200 Subject: [PATCH 05/20] Clarify Suspense image example --- src/content/reference/react/Suspense.md | 66 ++++++++++++------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index 18b20816f8f..f4b6fb061d8 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3053,21 +3053,23 @@ hr { When a [``](/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 `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. -In the example below, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. - -For comparison, the second button performs the same update without React. Nothing waits for the image, so the card appears immediately and the image pops in when it loads: +In this example, [`startTransition`](/reference/react/startTransition) marks both state updates as Transitions. Compare what happens when React renders the image inside and outside a ``: ```js -import { ViewTransition, Suspense, useState, startTransition } from 'react'; +import { + ViewTransition, + Suspense, + useState, + startTransition, +} from 'react'; import { freshImageUrl } from './image.js'; -import VanillaProfile from './VanillaProfile.js'; function Profile({ src }) { return (
- Jack Pope +

Jack Pope

); @@ -3083,47 +3085,39 @@ function ProfilePlaceholder() { } export default function App() { - const [src, setSrc] = useState(null); + const [viewTransitionSrc, setViewTransitionSrc] = useState(null); + const [plainSrc, setPlainSrc] = useState(null); return ( <> - {src && ( + {viewTransitionSrc && ( }> - + )}
- - - ); -} -``` - -```js src/VanillaProfile.js -import { useRef } from 'react'; -import { freshImageUrl } from './image.js'; - -export default function VanillaProfile() { - const ref = useRef(null); - function show() { - ref.current.innerHTML = `
- Jack Pope -

Jack Pope

-
`; - } - return ( - <> - -
+ + {plainSrc && ( + }> + + + )} ); } @@ -3180,6 +3174,8 @@ hr { +Inside ``, React keeps the skeleton visible while it waits for the image, up to a timeout. Outside it, React does not wait for the image before committing the card. + --- ### Coordinating fonts, images, and stylesheets {/*coordinating-fonts-images-and-stylesheets*/} @@ -3202,7 +3198,7 @@ function ProfileCard({ resources }) { <>
- Jack Pope +

Jack Pope

{quote}

@@ -3274,7 +3270,7 @@ export default function VanillaProfileCard() { .bio { margin: 0; font-family: 'Caveat', sans-serif; font-size: 20px; line-height: 26px; }
- Jack Pope +

Jack Pope

${quote}

From c2bc706df3ebf1c56aaa3e9b461430423685cb20 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 21:59:42 +0200 Subject: [PATCH 06/20] Simplify View Transition comparison examples --- .../reference/react-dom/components/img.md | 50 +++++++++++++------ src/content/reference/react/Suspense.md | 50 +++++++++++++------ 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index b505c1e6f46..4ddbb4fc0c9 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -160,43 +160,63 @@ function ProfilePlaceholder() { ); } -export default function App() { - const [viewTransitionSrc, setViewTransitionSrc] = useState(null); - const [plainSrc, setPlainSrc] = useState(null); +function ProfileInViewTransition({ src }) { + return ( + + }> + + + + ); +} + +function ProfileWithViewTransition() { + const [src, setSrc] = useState(null); return ( <> - {viewTransitionSrc && ( - - }> - - - - )} -
+ {src && } + + ); +} + +function ProfileWithoutViewTransition() { + const [src, setSrc] = useState(null); + return ( + <> - {plainSrc && ( + {src && ( }> - + )} ); } + +export default function App() { + return ( + <> + +
+ + + ); +} ``` ```js src/image.js hidden diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index f4b6fb061d8..b42825b9c1a 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3084,43 +3084,63 @@ function ProfilePlaceholder() { ); } -export default function App() { - const [viewTransitionSrc, setViewTransitionSrc] = useState(null); - const [plainSrc, setPlainSrc] = useState(null); +function ProfileInViewTransition({ src }) { + return ( + + }> + + + + ); +} + +function ProfileWithViewTransition() { + const [src, setSrc] = useState(null); return ( <> - {viewTransitionSrc && ( - - }> - - - - )} -
+ {src && } + + ); +} + +function ProfileWithoutViewTransition() { + const [src, setSrc] = useState(null); + return ( + <> - {plainSrc && ( + {src && ( }> - + )} ); } + +export default function App() { + return ( + <> + +
+ + + ); +} ``` ```js src/image.js hidden From a7e74ea9f7bd8a19e5f5e2d5e75ebbd5b57e28a8 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 22:10:52 +0200 Subject: [PATCH 07/20] Use a Suspense load in the image example --- src/content/reference/react/Suspense.md | 63 ++++++++++++------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index b42825b9c1a..2992a00e577 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3053,7 +3053,7 @@ hr { When a [``](/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 `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. -In this example, [`startTransition`](/reference/react/startTransition) marks both state updates as Transitions. Compare what happens when React renders the image inside and outside a ``: +The two buttons load a profile inside and outside a ``: @@ -3061,16 +3061,17 @@ In this example, [`startTransition`](/reference/react/startTransition) marks bot import { ViewTransition, Suspense, + use, useState, - startTransition, } from 'react'; -import { freshImageUrl } from './image.js'; +import { fetchProfile } from './data.js'; -function Profile({ src }) { +function Profile({ profilePromise }) { + const profile = use(profilePromise); return (
- -

Jack Pope

+ +

{profile.name}

); } @@ -3084,48 +3085,40 @@ function ProfilePlaceholder() { ); } -function ProfileInViewTransition({ src }) { +function ProfileInViewTransition({ profilePromise }) { return ( }> - + ); } function ProfileWithViewTransition() { - const [src, setSrc] = useState(null); + const [profilePromise, setProfilePromise] = useState(null); return ( <> - - {src && } + {profilePromise && ( + + )} ); } function ProfileWithoutViewTransition() { - const [src, setSrc] = useState(null); + const [profilePromise, setProfilePromise] = useState(null); return ( <> - - {src && ( + {profilePromise && ( }> - + )} @@ -3143,11 +3136,15 @@ export default function App() { } ``` -```js src/image.js hidden -// Add a unique parameter so the image isn't cached, -// and every run shows the loading state. -export function freshImageUrl() { - return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); +```js src/data.js hidden +export async function fetchProfile() { + // Add a fake delay so the Suspense fallback is visible. + await new Promise(resolve => setTimeout(resolve, 1000)); + return { + name: 'Jack Pope', + // Add a unique parameter so the image isn't cached. + image: 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(), + }; } ``` @@ -3185,8 +3182,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } @@ -3194,7 +3191,7 @@ hr {
-Inside ``, React keeps the skeleton visible while it waits for the image, up to a timeout. Outside it, React does not wait for the image before committing the card. +When the Promise resolves, React retries rendering the suspended content. This retry activates the enclosing ``, so this example does not need `startTransition`. Inside ``, React keeps the skeleton visible while it waits for the image, up to a timeout. Outside it, React does not wait for the image before committing the card. --- From eebc8f20c122452a430cafc635cf47c821580078 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Wed, 23 Sep 2026 22:23:14 +0200 Subject: [PATCH 08/20] Align Suspense resource loading examples --- src/content/reference/react/Suspense.md | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index 2992a00e577..22d7ec1080a 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -2910,7 +2910,7 @@ For comparison, the second button performs the same update without React. Nothin ```js -import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; +import { ViewTransition, Suspense, use, useState } from 'react'; import { fetchQuote } from './data.js'; import { freshFontUrl } from './font.js'; import VanillaQuote from './VanillaQuote.js'; @@ -2935,12 +2935,7 @@ export default function App() { const [fontSrc, setFontSrc] = useState(null); return ( <> - {fontSrc && ( @@ -3038,8 +3033,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } @@ -3047,6 +3042,8 @@ hr { +When the Promise resolves, React retries rendering the suspended content. This retry activates the enclosing ``, so this example does not need `startTransition`. + --- ### Waiting for an image to load {/*waiting-for-an-image-to-load*/} @@ -3204,7 +3201,7 @@ For comparison, the version without React loads the same data and shows every re ```js -import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; +import { ViewTransition, Suspense, use, useState } from 'react'; import { fetchQuote } from './data.js'; import { freshStylesheetUrl, freshImageUrl } from './resources.js'; import VanillaProfileCard from './VanillaProfileCard.js'; @@ -3243,12 +3240,10 @@ export default function App() { <> - {src && } - - ); -} - -function ProfileWithoutViewTransition() { - const [src, setSrc] = useState(null); - return ( - <> + {viewTransitionSrc && ( + + )} +
{fontSrc && ( @@ -3033,8 +3038,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0", - "react-dom": "19.3.0", + "react": "19.3.0-canary-f1f7ed2a-20260904", + "react-dom": "19.3.0-canary-f1f7ed2a-20260904", "react-scripts": "latest" } } @@ -3042,33 +3047,28 @@ hr {
-When the Promise resolves, React retries rendering the suspended content. This retry activates the enclosing ``, so this example does not need `startTransition`. - --- ### Waiting for an image to load {/*waiting-for-an-image-to-load*/} When a [``](/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 `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. -The two buttons load a profile inside and outside a ``: +In the example below, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. + +For comparison, the second button performs the same update outside a ``. When the Promise resolves, React reveals the card without waiting for the image, so the image may pop in after the card appears: ```js -import { - ViewTransition, - Suspense, - use, - useState, -} from 'react'; -import { fetchProfile } from './data.js'; +import { ViewTransition, Suspense, use, useState } from 'react'; +import { fetchImageSrc } from './image.js'; -function Profile({ profilePromise }) { - const profile = use(profilePromise); +function Profile({ srcPromise }) { + const src = use(srcPromise); return (
- -

{profile.name}

+ Jack Pope +

Jack Pope

); } @@ -3082,66 +3082,47 @@ function ProfilePlaceholder() { ); } -function ProfileInViewTransition({ profilePromise }) { +function ProfileInViewTransition({ srcPromise }) { return ( }> - + ); } -function ProfileWithViewTransition() { - const [profilePromise, setProfilePromise] = useState(null); +export default function App() { + const [transitionSrcPromise, setTransitionSrcPromise] = useState(null); + const [srcPromise, setSrcPromise] = useState(null); return ( <> - - {profilePromise && ( - + {transitionSrcPromise && ( + )} - - ); -} - -function ProfileWithoutViewTransition() { - const [profilePromise, setProfilePromise] = useState(null); - return ( - <> - - {profilePromise && ( + {srcPromise && ( }> - + )} ); } - -export default function App() { - return ( - <> - -
- - - ); -} ``` -```js src/data.js hidden -export async function fetchProfile() { +```js src/image.js hidden +export async function fetchImageSrc() { // Add a fake delay so the Suspense fallback is visible. await new Promise(resolve => setTimeout(resolve, 1000)); - return { - name: 'Jack Pope', - // Add a unique parameter so the image isn't cached. - image: 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(), - }; + // Add a unique parameter so the image isn't cached. + return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); } ``` @@ -3179,8 +3160,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0", - "react-dom": "19.3.0", + "react": "19.3.0-canary-f1f7ed2a-20260904", + "react-dom": "19.3.0-canary-f1f7ed2a-20260904", "react-scripts": "latest" } } @@ -3188,7 +3169,7 @@ hr {
-When the Promise resolves, React retries rendering the suspended content. This retry activates the enclosing ``, so this example does not need `startTransition`. Inside ``, React keeps the skeleton visible while it waits for the image, up to a timeout. Outside it, React does not wait for the image before committing the card. +When the Promise resolves, React retries rendering the suspended content. This retry activates the enclosing ``, so this example does not need `startTransition`. --- @@ -3201,7 +3182,7 @@ For comparison, the version without React loads the same data and shows every re ```js -import { ViewTransition, Suspense, use, useState } from 'react'; +import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; import { fetchQuote } from './data.js'; import { freshStylesheetUrl, freshImageUrl } from './resources.js'; import VanillaProfileCard from './VanillaProfileCard.js'; @@ -3212,7 +3193,7 @@ function ProfileCard({ resources }) { <>
- + Jack Pope

Jack Pope

{quote}

@@ -3240,10 +3221,12 @@ export default function App() { <> {src && ( - }> - - + )} ); @@ -223,17 +210,6 @@ export function freshImageUrl() { .card p { font-weight: bold; } -.avatar-placeholder { - width: 80px; - height: 80px; - border-radius: 50%; - background: #dfe3e9; -} -.name-placeholder { - width: 90px; - border-radius: 4px; - background: #dfe3e9; -} hr { margin: 16px 0; } diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index b32ef88ce48..9f158f4ce5f 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3051,7 +3051,7 @@ hr { ### Waiting for an image to load {/*waiting-for-an-image-to-load*/} -When a [``](/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 `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. +A Suspense boundary can reveal its content inside a [``](/reference/react/ViewTransition). During that reveal, React waits for visible images to load, up to a timeout, so the animation doesn't start with a half-loaded image. Adding an `onLoad` handler opts a specific image out. In the example below, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. @@ -3167,8 +3167,6 @@ hr { -When the Promise resolves, React retries rendering the suspended content. Because the Suspense boundary is wrapped in ``, React waits for the image before starting the animation. - --- ### Coordinating fonts, images, and stylesheets {/*coordinating-fonts-images-and-stylesheets*/} diff --git a/src/content/reference/react/ViewTransition.md b/src/content/reference/react/ViewTransition.md index 1011a1c9623..8873183c6ae 100644 --- a/src/content/reference/react/ViewTransition.md +++ b/src/content/reference/react/ViewTransition.md @@ -1253,9 +1253,9 @@ It's important to properly use keys to preserve identity when reordering lists. ### Animating from Suspense content {/*animating-from-suspense-content*/} -Like any Transition, React waits for data and new CSS (``) 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. +`` can animate the reveal of a Suspense boundary. When content suspends inside a new boundary, React first shows the fallback. When the content is ready, the enclosing `` animates the reveal. -If it's inside a new Suspense boundary instance, then the fallback is shown first. After the Suspense boundary fully loads, it triggers the `` to animate the reveal to the content. +Before starting the animation, React also waits for new stylesheets, fonts, and images introduced by the content to load, up to their respective timeouts. This prevents them from appearing partway through the animation. 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. There are two ways to animate Suspense boundaries depending on where you place the ``: @@ -1315,7 +1315,7 @@ export function VideoPlaceholder() { ``` ```js -import {ViewTransition, useState, startTransition, Suspense} from 'react'; +import {ViewTransition, useState, Suspense} from 'react'; import {Video, VideoPlaceholder} from './Video'; import {useLazyVideoData} from './data'; @@ -1328,12 +1328,7 @@ export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> - {showItem ? ( From f0392642dec641ba3070f2ac680cba3d8c18d9bd Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 24 Sep 2026 12:13:19 +0200 Subject: [PATCH 13/20] Use Suspense for View Transition image example --- .../reference/react-dom/components/img.md | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index 4c0cb1a21d0..a9e406c4b7a 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -125,19 +125,18 @@ To create an explicit preload hint, call [`preload`](/reference/react-dom/preloa ### Waiting for an image during a View Transition {/*waiting-for-an-image-during-a-view-transition*/} -During a client-rendered [``](/reference/react/ViewTransition) update, React may wait for an image to load and decode before starting the animation. This applies when a new `` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `` subtree and must not have `loading="lazy"` or an `onLoad` handler. React does not wait for images during synchronous updates. +When a Suspense boundary reveals content inside a client-rendered [``](/reference/react/ViewTransition), React may wait for a visible image to load and decode before starting the animation. This applies when a new `` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `` subtree and must not have `loading="lazy"` or an `onLoad` handler. React stops waiting after a timeout so that a slow image does not block the update indefinitely. -When a Suspense boundary reveals streamed content inside a ``, React may also wait for visible images with a non-empty `src` that do not have `loading="lazy"`. React stops waiting after a timeout so that a slow image does not block the update indefinitely. - -Because React does not wait for images during synchronous updates, this example wraps both state updates in [`startTransition`](/reference/react/startTransition). Compare what happens when React renders the image inside and outside a ``: +In this example, both versions suspend while reading the image source. Compare what happens to the `` when the Suspense boundary reveals its content inside and outside a ``: ```js -import { ViewTransition, useState, startTransition } from 'react'; -import { freshImageUrl } from './image.js'; +import { ViewTransition, Suspense, use, useState } from 'react'; +import { fetchImageSrc } from './image.js'; -function Profile({ src }) { +function Profile({ srcPromise }) { + const src = use(srcPromise); return (
Jack Pope @@ -146,41 +145,44 @@ function Profile({ src }) { ); } -function ProfileInViewTransition({ src }) { +function ProfilePlaceholder() { + return ( +
+
+

 

+
+ ); +} + +function ProfileInViewTransition({ srcPromise }) { return ( - + }> + + ); } export default function App() { - const [viewTransitionSrc, setViewTransitionSrc] = useState(null); - const [src, setSrc] = useState(null); + const [transitionSrcPromise, setTransitionSrcPromise] = useState(null); + const [srcPromise, setSrcPromise] = useState(null); return ( <> - - {viewTransitionSrc && ( - + {transitionSrcPromise && ( + )}
- - {src && ( - + {srcPromise && ( + }> + + )} ); @@ -188,9 +190,8 @@ export default function App() { ``` ```js src/image.js hidden -// Add a unique parameter so the image isn't cached, -// and every run shows the loading state. -export function freshImageUrl() { +export async function fetchImageSrc() { + // Add a unique parameter so the image isn't cached. return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); } ``` @@ -210,6 +211,17 @@ export function freshImageUrl() { .card p { font-weight: bold; } +.avatar-placeholder { + width: 80px; + height: 80px; + border-radius: 50%; + background: #dfe3e9; +} +.name-placeholder { + width: 90px; + border-radius: 4px; + background: #dfe3e9; +} hr { margin: 16px 0; } @@ -227,4 +239,4 @@ hr { -Inside ``, React waits for the image before committing the card, up to a timeout. Outside it, React commits the card without waiting for the image. +Inside ``, React waits for the image before committing the Suspense reveal, up to a timeout. Outside it, React reveals the card without waiting for the image. From 7c476e42de4986f03784603e8879a2211cd44862 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 24 Sep 2026 12:26:32 +0200 Subject: [PATCH 14/20] Make image loading comparison visible --- src/content/reference/react-dom/components/img.md | 2 ++ src/content/reference/react/Suspense.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index a9e406c4b7a..d0aeb48b49e 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -191,6 +191,8 @@ export default function App() { ```js src/image.js hidden export async function fetchImageSrc() { + // Add a fake delay so the Suspense fallback is visible. + await new Promise(resolve => setTimeout(resolve, 1000)); // Add a unique parameter so the image isn't cached. return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); } diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index 9f158f4ce5f..2a26dc7ed7e 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3119,6 +3119,8 @@ export default function App() { ```js src/image.js hidden export async function fetchImageSrc() { + // Add a fake delay so the Suspense fallback is visible. + await new Promise(resolve => setTimeout(resolve, 1000)); // Add a unique parameter so the image isn't cached. return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); } From ca0b11a9ac933b44f9b7f1d801067a32c84e8835 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 24 Sep 2026 12:49:25 +0200 Subject: [PATCH 15/20] Tighten View Transition image examples --- .../reference/react-dom/components/img.md | 37 +++++++++-------- src/content/reference/react/Suspense.md | 41 +++++++++---------- src/content/reference/react/ViewTransition.md | 10 ++--- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index d0aeb48b49e..198a7036eb7 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -125,14 +125,19 @@ To create an explicit preload hint, call [`preload`](/reference/react-dom/preloa ### Waiting for an image during a View Transition {/*waiting-for-an-image-during-a-view-transition*/} -When a Suspense boundary reveals content inside a client-rendered [``](/reference/react/ViewTransition), React may wait for a visible image to load and decode before starting the animation. This applies when a new `` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `` subtree and must not have `loading="lazy"` or an `onLoad` handler. React stops waiting after a timeout so that a slow image does not block the update indefinitely. +During a Suspense reveal inside a [``](/reference/react/ViewTransition), React waits up to a timeout for visible images to load and decode before starting the animation. This includes newly rendered `` elements with a non-empty `src` and existing images whose `src` or `srcSet` changes. React does not wait for images with `loading="lazy"` or an `onLoad` handler. -In this example, both versions suspend while reading the image source. Compare what happens to the `` when the Suspense boundary reveals its content inside and outside a ``: +Compare how the same image appears when a Suspense boundary reveals its content inside and outside a ``: ```js -import { ViewTransition, Suspense, use, useState } from 'react'; +import { + ViewTransition, + Suspense, + use, + useState, +} from 'react'; import { fetchImageSrc } from './image.js'; function Profile({ srcPromise }) { @@ -154,26 +159,22 @@ function ProfilePlaceholder() { ); } -function ProfileInViewTransition({ srcPromise }) { - return ( - - }> - - - - ); -} - export default function App() { - const [transitionSrcPromise, setTransitionSrcPromise] = useState(null); + const [transitionSrcPromise, setTransitionSrcPromise] = + useState(null); const [srcPromise, setSrcPromise] = useState(null); return ( <> - {transitionSrcPromise && ( - + + }> + + + )}
{transitionSrcPromise && ( - + + }> + + + )}
- {transitionSrcPromise && ( + {showWithTransition && ( }> - + )}
- - {srcPromise && ( + {showWithoutTransition && ( }> - + )} @@ -191,7 +191,17 @@ export default function App() { ``` ```js src/image.js hidden -export async function fetchImageSrc() { +// Normally, the caching logic would be inside a framework. +const cache = new Map(); + +export function fetchImageSrc(cacheKey) { + if (!cache.has(cacheKey)) { + cache.set(cacheKey, loadImageSrc()); + } + return cache.get(cacheKey); +} + +async function loadImageSrc() { // Delay the response so the Suspense fallback is visible. await new Promise(resolve => setTimeout(resolve, 1000)); // Add a unique parameter so the image isn't cached. diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index bef0023d9cd..e781effdc19 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3066,8 +3066,8 @@ import { } from 'react'; import { fetchImageSrc } from './image.js'; -function Profile({ srcPromise }) { - const src = use(srcPromise); +function Profile({ cacheKey }) { + const src = use(fetchImageSrc(cacheKey)); return (
Jack Pope @@ -3086,29 +3086,29 @@ function ProfilePlaceholder() { } export default function App() { - const [transitionSrcPromise, setTransitionSrcPromise] = - useState(null); - const [srcPromise, setSrcPromise] = useState(null); + const [showWithTransition, setShowWithTransition] = + useState(false); + const [showWithoutTransition, setShowWithoutTransition] = + useState(false); return ( <> - - {transitionSrcPromise && ( + {showWithTransition && ( }> - + )}
- - {srcPromise && ( + {showWithoutTransition && ( }> - + )} @@ -3117,7 +3117,17 @@ export default function App() { ``` ```js src/image.js hidden -export async function fetchImageSrc() { +// Normally, the caching logic would be inside a framework. +const cache = new Map(); + +export function fetchImageSrc(cacheKey) { + if (!cache.has(cacheKey)) { + cache.set(cacheKey, loadImageSrc()); + } + return cache.get(cacheKey); +} + +async function loadImageSrc() { // Delay the response so the Suspense fallback is visible. await new Promise(resolve => setTimeout(resolve, 1000)); // Add a unique parameter so the image isn't cached. From 2b0bf468a50f341b1f02d3149e951c3a069de4c7 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 24 Sep 2026 15:25:54 +0200 Subject: [PATCH 18/20] Fix Suspense resource demos --- src/content/reference/react/Suspense.md | 43 +++++++++++++++++-------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index e781effdc19..f54c405527f 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -2903,7 +2903,7 @@ Where you place the `` relative to the boundary determines wheth When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits up to 500 ms for new fonts the content introduces so the text doesn't flash with a fallback font. This only happens during a `` update. -In the example below, the Suspense boundary is wrapped in a ``, 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. +In the example below, the Suspense boundary is wrapped in a ``, and the `Quote` component suspends while its data loads. Rendering the quote starts its font download. React keeps the fallback visible while the font loads. If it loads within the 500 ms limit, the quote appears already in its font. For comparison, the second button performs the same update without React. Nothing waits for the font, so the text appears in a fallback font first and then switches: @@ -2922,7 +2922,7 @@ function Quote({ fontSrc }) { @@ -2967,7 +2967,7 @@ export default function VanillaQuote() { const style = document.createElement('style'); style.textContent = `@font-face { font-family: 'VanillaFancy'; - src: url(${freshFontUrl()}) format('truetype'); + src: url(${freshFontUrl()}) format('woff2'); font-display: swap; }`; document.head.appendChild(style); @@ -2987,7 +2987,7 @@ export default function VanillaQuote() { // and every run shows the loading state. export function freshFontUrl() { return ( - 'https://raw.githubusercontent.com/google/fonts/main/ofl/caveat/Caveat%5Bwght%5D.ttf' + + 'https://fonts.gstatic.com/s/caveat/v23/WnznHAc5bAfYB2QRah7pcpNvOx-pjfJ9eIWpYT5Kmgq3sw.woff2' + '?t=' + Date.now() ); @@ -3038,8 +3038,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } @@ -3260,7 +3260,6 @@ import { freshStylesheetUrl, freshImageUrl } from './resources.js'; export default function VanillaProfileCard() { const ref = useRef(null); async function show() { - const quote = await fetchQuote(); const doc = ref.current.contentWindow.document; doc.open(); doc.write(` @@ -3270,17 +3269,34 @@ export default function VanillaProfileCard() { .profile-card img { border-radius: 50%; background: #dfe3e9; } .name { margin: 0 0 4px; font-family: 'Caveat', sans-serif; font-size: 22px; line-height: 28px; font-weight: bold; } .bio { margin: 0; font-family: 'Caveat', sans-serif; font-size: 20px; line-height: 26px; } + .avatar-placeholder { width: 80px; height: 80px; border-radius: 50%; background: #dfe3e9; } + .name-placeholder, .bio-placeholder { border-radius: 4px; background: #dfe3e9; color: transparent; } + .name-placeholder { width: 90px; } + .bio-placeholder { width: 220px; height: 52px; }
- Jack Pope +
-

Jack Pope

-

${quote}

+

 

+

 

- `); doc.close(); + + const quote = await fetchQuote(); + doc.body.innerHTML = ` +
+ Jack Pope +
+

Jack Pope

+

${quote}

+
+
`; + const stylesheet = doc.createElement('link'); + stylesheet.rel = 'stylesheet'; + stylesheet.href = freshStylesheetUrl(); + doc.head.appendChild(stylesheet); } return ( <> @@ -3373,6 +3389,7 @@ hr { } .bio-placeholder { width: 220px; + height: 52px; } .vanilla-frame { display: block; @@ -3386,8 +3403,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } From 4502bd29899ffa87a1d7f80d651bfb99dd1c10fb Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 24 Sep 2026 15:30:13 +0200 Subject: [PATCH 19/20] Clarify View Transition image reveal --- src/content/reference/react-dom/components/img.md | 2 +- src/content/reference/react/Suspense.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index 0853381fbcc..cb6df0d4509 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -252,4 +252,4 @@ hr { -Inside ``, React waits for the image before revealing the card. Outside it, React reveals the card without waiting for the image. +With ``, React keeps the skeleton visible for up to 500 ms while the image loads, so the card can be revealed with its image already in place. Without ``, Suspense stops showing the skeleton as soon as the Promise resolves. If the image is still loading, the card appears first and the image pops in afterward. diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index f54c405527f..d6fd5c867aa 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -3053,7 +3053,7 @@ hr { When a Suspense boundary reveals content inside a [``](/reference/react/ViewTransition), React waits up to 500 ms for visible images to load before starting the animation. An `onLoad` handler opts an image out. -Compare the same boundary inside and outside a ``. Inside, React keeps the profile skeleton visible until the image is ready. Outside, React reveals the card when the Promise resolves, so the image may appear afterward: +Compare the same boundary inside and outside a ``. Inside, React keeps the profile skeleton visible for up to 500 ms while the image loads, so the card can be revealed with its image already in place. Outside, Suspense stops showing the skeleton as soon as the Promise resolves. If the image is still loading, the card appears first and the image pops in afterward: From bd35db899ffcf139ffed5e7c74f5c2daa0ba8347 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Thu, 24 Sep 2026 15:36:39 +0200 Subject: [PATCH 20/20] Fix Suspense font loading example --- src/content/reference/react/Suspense.md | 51 ++++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index d6fd5c867aa..50039d4f561 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -2903,50 +2903,57 @@ Where you place the `` relative to the boundary determines wheth When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits up to 500 ms for new fonts the content introduces so the text doesn't flash with a fallback font. This only happens during a `` update. -In the example below, the Suspense boundary is wrapped in a ``, and the `Quote` component suspends while its data loads. Rendering the quote starts its font download. React keeps the fallback visible while the font loads. If it loads within the 500 ms limit, the quote appears already in its font. +In the example below, the Suspense boundary is wrapped in a ``, and the `Quote` component suspends while its data loads. Rendering the quote loads a stylesheet that introduces the font. React keeps the fallback visible while the stylesheet and font load. If the font loads within the 500 ms limit, the quote appears already in its font. For comparison, the second button performs the same update without React. Nothing waits for the font, so the text appears in a fallback font first and then switches: ```js -import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; +import { + ViewTransition, + Suspense, + use, + useState, + startTransition, +} from 'react'; import { fetchQuote } from './data.js'; -import { freshFontUrl } from './font.js'; +import { + freshFontUrl, + freshStylesheetUrl, +} from './font.js'; import VanillaQuote from './VanillaQuote.js'; -function Quote({ fontSrc }) { +function Quote({ stylesheet }) { const quote = use(fetchQuote()); return ( <> - +

{quote}

); } export default function App() { - const [fontSrc, setFontSrc] = useState(null); + const [stylesheet, setStylesheet] = useState(null); return ( <> - {fontSrc && ( + {stylesheet && ( ⌛ Loading quote...

}> - +
)} @@ -2983,9 +2990,17 @@ export default function VanillaQuote() { ``` ```js src/font.js hidden -// Add a unique parameter so the font isn't cached, -// and every run shows the loading state. +export function freshStylesheetUrl() { + // Add a unique parameter so the stylesheet isn't cached. + return ( + 'https://fonts.googleapis.com/css2?family=Caveat&display=swap' + + '&t=' + + Date.now() + ); +} + export function freshFontUrl() { + // Add a unique parameter so the font isn't cached. return ( 'https://fonts.gstatic.com/s/caveat/v23/WnznHAc5bAfYB2QRah7pcpNvOx-pjfJ9eIWpYT5Kmgq3sw.woff2' + '?t=' + @@ -3025,7 +3040,7 @@ export function fetchQuote() { margin-top: 1em; } .fancy { - font-family: 'Fancy', sans-serif; + font-family: 'Caveat', sans-serif; } .vanilla-fancy { font-family: 'VanillaFancy', sans-serif;