Welcome to the EpicWeb.dev Workshop app!

This is the deployed version. Run locally for full experience.

Suspense img

You can suspend more than just fetch requests with Suspense and the use hook. Anything async can be suspended, including images.
But, why would you want to suspend on images? Well, let's look at an example:
<img src="/some/image.jpg" />
If we were to change the src of the image, the browser would start loading the new image and only replace the old image when the new one has finished downloading. This is probably what you want (you wouldn't want to get a flicker of no image at all while the new image is loading).
The trouble is, what if in addition to rendering an updated image, there's updated content as well? The user would see the old image displayed alongside the new content until the new image is loaded. This is easier to visualize, so take this for example:
In the video above, the network speed has been artificially slowed down to a 3G network speed and the cache has also been cleared. As a result, once a different space ship is selected, the data request for the new ship takes about 2 seconds. Once the data has loaded, React re-renders the UI with the new data, including the img src attribute. However, the browser leaves the old src attribute in place to avoid a flicker of no image at all while the new image is loading.
The new image takes almost 10 seconds to download, all the while, the old image is still being displayed. This could be confusing to users and not a great user experience. It would be better for us to have more control over that experience.
All you need to do to make this work with suspense is have some mechanism to load the image in an async function. When the promise resolves, you know the image is ready to be resolved. We can actually take advantage of the browser's built-in caching mechanism to make this work by assuming that if we request the image twice, the second request will be faster because the image is cached.
So all we need to do is create an image, set it's src, and set the onload and onerror event handlers to resolve or reject a promise. Here's an example:
function preloadImage(src: string) {
	return new Promise<string>(async (resolve, reject) => {
		const img = new Image()
		img.src = src
		img.onload = () => resolve(src)
		img.onerror = reject
	})
}
Now, you can call that function to get a promise that resolves when the image has been loaded (and presumably is in the browser cache assuming cache headers are set properly). So you can use that for a custom Img component to suspend on the image.
There are other things to consider here because now you're preventing the data from being displayed until the image is ready which may not be what you want either. But React gives us the ability to control this as well, by adding a Suspense boundary around the Img component and adding a key prop to the Suspense boundary (or a parent) to force it to render the fallback for just the image which will allow us to display the data while the image is loading.
Ultimately leading us to this improved experience:
In the video above, all the same network conditions are in effect, but now once the data finishes loading, the old image is replaced by a fallback image (which was already loaded earlier on the initial page load and is in the cache) and the new image is displayed once it's ready.
This resolves the confusion and leads to a better user experience.
And it's all thanks to React's ability to suspend on any kid of async operation.