Skip to content

Commit f039264

Browse files
committed
Use Suspense for View Transition image example
1 parent 3044159 commit f039264

1 file changed

Lines changed: 43 additions & 31 deletions

File tree

  • src/content/reference/react-dom/components

‎src/content/reference/react-dom/components/img.md‎

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,18 @@ To create an explicit preload hint, call [`preload`](/reference/react-dom/preloa
125125

126126
### Waiting for an image during a View Transition {/*waiting-for-an-image-during-a-view-transition*/}
127127

128-
During a client-rendered [`<ViewTransition>`](/reference/react/ViewTransition) update, React may wait for an image to load and decode before starting the animation. This applies when a new `<img>` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `<ViewTransition>` subtree and must not have `loading="lazy"` or an `onLoad` handler. React does not wait for images during synchronous updates.
128+
When a Suspense boundary reveals content inside a client-rendered [`<ViewTransition>`](/reference/react/ViewTransition), React may wait for a visible image to load and decode before starting the animation. This applies when a new `<img>` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `<ViewTransition>` 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.
129129

130-
When a Suspense boundary reveals streamed content inside a `<ViewTransition>`, 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.
131-
132-
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 `<ViewTransition>`:
130+
In this example, both versions suspend while reading the image source. Compare what happens to the `<img>` when the Suspense boundary reveals its content inside and outside a `<ViewTransition>`:
133131

134132
<Sandpack>
135133

136134
```js
137-
import { ViewTransition, useState, startTransition } from 'react';
138-
import { freshImageUrl } from './image.js';
135+
import { ViewTransition, Suspense, use, useState } from 'react';
136+
import { fetchImageSrc } from './image.js';
139137

140-
function Profile({ src }) {
138+
function Profile({ srcPromise }) {
139+
const src = use(srcPromise);
141140
return (
142141
<div className="card">
143142
<img src={src} alt="Jack Pope" width={80} height={80} />
@@ -146,51 +145,53 @@ function Profile({ src }) {
146145
);
147146
}
148147

149-
function ProfileInViewTransition({ src }) {
148+
function ProfilePlaceholder() {
149+
return (
150+
<div className="card">
151+
<div className="avatar-placeholder" />
152+
<p className="name-placeholder">&nbsp;</p>
153+
</div>
154+
);
155+
}
156+
157+
function ProfileInViewTransition({ srcPromise }) {
150158
return (
151159
<ViewTransition>
152-
<Profile src={src} />
160+
<Suspense fallback={<ProfilePlaceholder />}>
161+
<Profile srcPromise={srcPromise} />
162+
</Suspense>
153163
</ViewTransition>
154164
);
155165
}
156166

157167
export default function App() {
158-
const [viewTransitionSrc, setViewTransitionSrc] = useState(null);
159-
const [src, setSrc] = useState(null);
168+
const [transitionSrcPromise, setTransitionSrcPromise] = useState(null);
169+
const [srcPromise, setSrcPromise] = useState(null);
160170
return (
161171
<>
162-
<button
163-
onClick={() => {
164-
startTransition(() => {
165-
setViewTransitionSrc(freshImageUrl());
166-
});
167-
}}>
172+
<button onClick={() => setTransitionSrcPromise(fetchImageSrc())}>
168173
Show profile with View Transition
169174
</button>
170-
{viewTransitionSrc && (
171-
<ProfileInViewTransition src={viewTransitionSrc} />
175+
{transitionSrcPromise && (
176+
<ProfileInViewTransition srcPromise={transitionSrcPromise} />
172177
)}
173178
<hr />
174-
<button
175-
onClick={() => {
176-
startTransition(() => {
177-
setSrc(freshImageUrl());
178-
});
179-
}}>
179+
<button onClick={() => setSrcPromise(fetchImageSrc())}>
180180
Show profile without View Transition
181181
</button>
182-
{src && (
183-
<Profile src={src} />
182+
{srcPromise && (
183+
<Suspense fallback={<ProfilePlaceholder />}>
184+
<Profile srcPromise={srcPromise} />
185+
</Suspense>
184186
)}
185187
</>
186188
);
187189
}
188190
```
189191

190192
```js src/image.js hidden
191-
// Add a unique parameter so the image isn't cached,
192-
// and every run shows the loading state.
193-
export function freshImageUrl() {
193+
export async function fetchImageSrc() {
194+
// Add a unique parameter so the image isn't cached.
194195
return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now();
195196
}
196197
```
@@ -210,6 +211,17 @@ export function freshImageUrl() {
210211
.card p {
211212
font-weight: bold;
212213
}
214+
.avatar-placeholder {
215+
width: 80px;
216+
height: 80px;
217+
border-radius: 50%;
218+
background: #dfe3e9;
219+
}
220+
.name-placeholder {
221+
width: 90px;
222+
border-radius: 4px;
223+
background: #dfe3e9;
224+
}
213225
hr {
214226
margin: 16px 0;
215227
}
@@ -227,4 +239,4 @@ hr {
227239

228240
</Sandpack>
229241

230-
Inside `<ViewTransition>`, React waits for the image before committing the card, up to a timeout. Outside it, React commits the card without waiting for the image.
242+
Inside `<ViewTransition>`, 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.

0 commit comments

Comments
 (0)