🖼️ Smooth & Reliable Image Loading with React Suspense
React Suspense has revolutionized how we handle asynchronous behavior in UI—especially when fetching data. But did you know you can also suspend image loading to create smoother and more predictable visual transitions?
This guide will walk you through:
key
propsLet’s start by understanding the problem.
Here’s a scenario:
This results in a confusing user experience: the content changes, but the image doesn’t update right away.
🎥 Watch the problem — the ship name and stats change, but the image lags far behind.
We want:
React Suspense gives us the tools to accomplish exactly that.
React Suspense lets us suspend rendering on any async operation—not just data fetching.
So how do we suspend for images?
By preloading them manually:
function preloadImage(src: string) {return new Promise<string>((resolve, reject) => {const img = new Image()img.src = srcimg.onload = () => resolve(src)img.onerror = reject})}
Now you can wrap this in a cache to avoid reloading the same image:
const imgCache = new Map<string, Promise<string>>()function getImgSrc(src: string) {if (!imgCache.has(src)) {imgCache.set(src, preloadImage(src))}return imgCache.get(src)!}
Then use React’s use()
hook inside your custom <Img>
component:
function Img({ src, alt }: { src: string; alt: string }) {const loadedSrc = use(getImgSrc(src))return <img src={loadedSrc} alt={alt} />}
By default, React suspends the whole subtree when something inside it suspends. So to suspend only the image, wrap your <Img>
in a Suspense
boundary:
<Suspense fallback={<FallbackImage />}><Img src={ship.imageUrl} alt={ship.name} /></Suspense>
That’s great… but what happens when the image fails to load?
If an image fails to load (e.g. due to a bad URL or offline connection), React will throw inside use()
. If that happens inside Img
, we want to show a fallback image—not crash the entire ship component!
Here’s how to make an image-specific error boundary:
function ShipImg({ src, alt }: { src: string; alt: string }) {return (<ErrorBoundary fallback={<img src={src} alt={alt} />}><Suspense fallback={<FallbackImage />}><Img src={src} alt={alt} /></Suspense></ErrorBoundary>)}
This ensures:
key
You might still run into a UX problem:
The UI waits for both data and image to load before updating the screen. That’s not ideal.
Why? Because Suspense boundaries inside a transition (useTransition
) won’t show their fallback. They’ll keep the old UI until everything is ready.
key
to the Suspense BoundaryBy giving your Suspense
(or ErrorBoundary
) a dynamic key
, you tell React:
“This is a brand-new boundary—treat it like an initial render.”
React will then show the fallback for just that Suspense boundary, even while other parts of the UI transition smoothly.
function ShipImg({ src, alt }: { src: string; alt: string }) {return (<ErrorBoundary fallback={<img src={src} alt={alt} />} key={src}><Suspense fallback={<FallbackImage />}><Img src={src} alt={alt} /></Suspense></ErrorBoundary>)}
✅ This shows ship data ASAP ✅ Shows image only when it’s loaded ✅ Prevents showing old images with new content
Feature | Technique |
---|---|
Preload image | new Image() with onload/onerror |
Suspend on load | Wrap preloadImage() in a cache and use use() |
Fallback UI | Wrap <Img> with <Suspense fallback={...}> |
Error fallback | Wrap Suspense with <ErrorBoundary> |
Fix transition behavior | Add key={src} to Suspense/ErrorBoundary |
function preloadImage(src: string): Promise<string> {return new Promise((resolve, reject) => {const img = new Image()img.src = srcimg.onload = () => resolve(src)img.onerror = reject})}const imgCache = new Map<string, Promise<string>>()function getImgSrc(src: string) {if (!imgCache.has(src)) {imgCache.set(src, preloadImage(src))}return imgCache.get(src)!}function Img({ src, alt }: { src: string; alt: string }) {const loadedSrc = use(getImgSrc(src))return <img src={loadedSrc} alt={alt} />}function ShipImg({ src, alt }: { src: string; alt: string }) {return (<ErrorBoundary fallback={<img src={src} alt={alt} />} key={src}><Suspense fallback={<FallbackImage />}><Img src={src} alt={alt} /></Suspense></ErrorBoundary>)}
Combine this with:
useTransition
for smooth UI while transitioning contentspin-delay
to delay spinners for brief loadsuseOptimistic
to show UI even before the async task resolvesLet your UI feel fast, smooth, and responsive—even when the network isn’t.
Quick Links
Legal Stuff
Social Media