Abstract
In this mini-series consisting of two posts I will build a React Component Image which, using custom hooks, shows a low-resolution image that is immediately replaced when the high-resolution counterpart is completely downloaded. In the second phase, I will take care of postponing the download of the second only when the component becomes visible
Table of content
- Low-resolution & High Resolution
- High-resolution only when is visible
Low-resolution & High-resolution
Concept
The rendering of a high-resolution image can take - especially for slow connections - several seconds. This lack of readiness results in worse UX
In this post, I deal with solving the problem by building a component that in addition to the high-resolution image source receives one for the low-resolution image to be shown as a replacement until the first is fully downloaded and available
In the next post, I will take care of postponing the download of the high-resolution image only when the component becomes visible within the view. Regardless, the user will not see a missing image as the relative low resolution will already be present
Process
In a project generated via create-react-app
I delete all that is superfluous
Then I initialize the construction of the Image
component
mkdir src/components touch src/components/Image.jsx
It is actually two <img>
placed one above the other and made visible alternately. To make them superimposable it is sufficient to use a wrapper with the necessary CSS properties. Furthermore, since the two images may have different sizes, it is recommended that while a wrapper defines width and height, the images contained therein adapt to its directives
Image.js
const Image = ({ width = '100%', height = '100%', lowResSrc, highResSrc }) => { const styles = { wrapper: { position: 'relative', width, height, }, image: { position: 'absolute', width: '100%', height: '100%', }, } return ( <div style={styles.wrapper}> <img src={lowResSrc} style={styles.image} /> <img src={highResSrc} style={styles.image} /> </div> ) } export default Image
Inline CSS is used rather than another solution for simplicity's sake
Now I use the component and I provide it with the required props
App.js (but it could be anywhere)
const srcTuple = [ 'https://via.placeholder.com/150', 'https://via.placeholder.com/600', ] ... <Image width={300} height={300} lowResSrc={srcTuple[0]} highResSrc={srcTuple[1]} />
At this point on the screen, there is the image related to srcTuple[0]
(the low-resolution source) because that is what the style wants. For the replacement to occur, it is necessary to be able to intervene when the download of the high-resolution image is completed
To do this I can use the onLoad
method of the<img>
attribute. The explanatory name indicates when it is performed
The question remains of what to actually make it perform
With a view to modern React, I decided to opt for a custom hook
It must keep track of the state of the image loading and on the basis of it return a style that leads to a pleasant transition between the two images of the component. To do this it must expose a method that will be associated with the onLoad
method
mkdir src/hooks touch src/hooks/useImageOnLoad.js
useImageOnLoad.js
import { useState } from 'react' const useImageOnLoad = () => { const [isLoaded, setIsLoaded] = useState(false) const handleImageOnLoad = () => setIsLoaded(true) const transitionStyles = { lowRes: { opacity: isLoaded ? 0 : 1, filter: 'blur(2px)', transition: 'opacity 500ms ease-out 50ms', }, highRes: { opacity: isLoaded ? 1 : 0, transition: 'opacity 500ms ease-in 50ms', }, } return { handleImageOnLoad, transitionStyles } } export default useImageOnLoad
So, just integrate the hook into the component. The method is associated with the onLoad
on the high resolution <img>
tag. The styles returned by the hook must be associated with its <img>
tags
Image.js (snellito)
const Image = ({ ... }) => { const { handleImageOnLoad, transitionStyles } = useImageOnLoad() const styles = {...} const lowResStyle = { ...styles.image, ...transitionStyles.lowRes } const hightResStyle = { ...styles.image, ...transitionStyles.highRes } return ( <div style={styles.wrapper}> <img src={lowResSrc} style={lowResStyle} /> <img src={highResSrc} style={hightResStyle} onLoad={handleImageOnLoad} /> </div> ) } export default Image
Considerations
Given the very little use of the network in this demo, to make the effect more appreciable it can be convenient
- multiply the number of
<Image />
components and their contents - simulate throttling in the Network tab of the Developer Tools
- disable cache
Finally, it is true that compared to a simple <img />
with a single source, <Image />
requires a few more bytes to be downloaded (AKA the low-resolution image). However, it's a small price to pay for a better UX, it's so true?
Thanks for reading, continue to the next post 🐨
Top comments (0)