Stable image component with placeholder in React
In most case, showing placeholders during loading images is a good idea, especially for slow network/devices. To build React apps with such an image component, there are several things to consider from both React and DOM aspect of view.
In this article, I will describe how to implement such a component by showing examples written in ES6.
*Updated 2018/07/21 Switched to ES6, Updated examples using reenhance-components
.
Showing a placeholder until ‘onload’ event fires
The most straightforward strategy to implement such a behavior will be:
- Initialize internal state with
false
- Set an
onLoad
event handler which updates state totrue
- Let the component show either placeholder or image depending on the state.
Because the placeholder must be displayed since the page loaded, it should be SVG or Data URI. We can embed SVG elements in regular JSX/TSX just by renaming attributes like view-box
to viewBox.
The state can be stored in StateProvider
from reenhance-components
. It provides state and updater by embedding it in JSX/TSX.
The whole procedure can be coded as following:
interface OuterProps {
src: string;
}const LoadedState = StateProvider(false);const ImageWithLoading = ({ src }) => (
<LoadedState>
{({ state: loaded, setState: setLoaded }) => (
<div>
{!loaded ? (
<svg width="100" height="100" viewBox="0 0 100 100">
<rect width="100" height="100" rx="10" ry="10" fill="#CCC" />
</svg>
) : null}
<img
src={src}
style={!loaded ? { visibility: 'hidden' } : {}}
onLoad={() => setLoaded(true)}
/>
</div>
)}
</LoadedState>
);
Did you notice that the placeholder and img
are hidden in different way? The placeholder can be removed from dom once loaded
became true but img
have to be there from beginning to fire onLoad
event.
The result will be like this.
You can also try it in this CodePen.
Looks good? Actually, this is not perfect yet.
Remounting causes image flickering
In React application, UI interaction may cause some update of application state which trigger remounting the image component. If the component was remounted, the loaded
state will be reset and result in flickering like this,
What can we do for this problem?
Reflecting DOM attribute to component state
This flickering happens because our component is waiting for the load
event regardless the image is *initially* loaded or not. That state is available through complete
DOM attribute which indicates the current state of element.
In order to access the attribute, we need a reference to the Image element. Since React 16.3, using createRef
is the recommended way. the ref
object can be stored & watched by ObjectWatcher
from reenhance-components
.
Once complete
be available, that value can be used to determine if the image is ready or not. It is described in the following code:
const { StateProvider, ObjectWatcher } = ReenhanceComponents;const LoadedState = StateProvider(false);
const ImageRefWatcher = ObjectWatcher(React.createRef());const ImageWithLoading = ({ src }) => (
<LoadedState>
{({ state: loaded, setState: setLoaded }) => (
<ImageRefWatcher watch="current">
{(imageRef) => {
const complete = imageRef.current && imageRef.current.complete; return (
<div>
{!complete ? (
<svg width="100" height="100" viewBox="0 0 100 100">
<rect width="100" height="100" rx="10" ry="10" fill="#CCC" />
</svg>
) : null}
<img
src={src}
style={!complete ? { visibility: 'hidden' } : {}}
ref={imageRef}
onLoad={() => setLoaded(true)}
/>
</div>
);
}}
</ImageRefWatcher>
)}
</LoadedState>
);
Notice that the loaded
state is not referred anymore. It is just for revoking rendering by calling setLoaded
.
The CodePen is here.
Exercise.
As we do not refer to loaded
state anymore, we can remove key
prop from ImageWithLoading
tag in the example above. Remove the key
prop and modify ImageWithLoading
so that it shows placeholder when new id is given. (Hint. Change the type of LoadedState
)
Conclusion
React works pretty well with stateless components but some HTML elements have hidden states behind them. You can take full control if you understand their behavior and wrap them up.
References
https://stackoverflow.com/questions/12354865/image-onload-event-and-browser-cache