5 Things to know about Images in React Native

The more I work with React Native images, the more I find it tricky. I wrote about Image Caching without Expo, and with Expo. In React Native Sketch Elements and React Native Fiber, I’m using react-native-expo-image-cache. You can see it in action below.

Progressive image loading and caching in React Native Sketch Elements.

react-native-expo-image-cache is new, fits well in my projects but might not be flexible enough yet to fit your requirements. If it doesn’t, please let me know in order to make this project evolve in the right direction. And in the meantime, you will find below a list of fives things you ought to know about Images in React Native.

Don’t use prefetch()

React Native provides out of the box Image.prefetch which can be useful to pre-load images but in some cases but it is somewhat inadequate.

You really need the image to be present locally in order to avoid any flickering effect when loading it. It also allows to have more control over the cache of images. In the example below, you see the difference from using the Image.prefetch Image cache versus serving the image locally.

With prefetch on the left and with cache on the right

You might also need to display your image in a different component than <Image>. For instance in the example below we use <SVGImage> which only works for local paths and or data URIs.

Use ExpoKit

So you need to store images locally. If you use expo already, there is a File system library out of the box. If your project is detached, the react-native-fetch-blob library seems to has lost its main contributor. Last time I used it, it had some issues. It might be worthwhile to add ExpoKit as a dependency of your project even if you are "detached" from the get go. ExpoKit ships many other great components, including BlurView which can be natively animated in case you also want to implement progressive image loading.

Dealing with concurrency on Android

Many components might display the same image at the same time. And doing a Filesystem.exists(localURI) operation on Android will return true even if the file download is not finished. This means that you need to implement an observer pattern to download each image only once and be notified when the image download has finished. In react-native-expo-image-cache, this is how the API looks like:

import {CacheManager} from "react-native-expo-image-cache";

// Remote URI
const {uri} = this.props;
CacheManager.cache(uri, localURI => this.setState({ uri: localURI }));

Below is the implementation of the observer:

static async cache(uri: string, listener: Listener): Promise<void> {
const {path, exists} = await getCacheEntry(uri);
// Is the image is already downloading, we just listen
if (isDownloading(uri)) {
addListener(uri, listener);
// If it's not downloading and it exists, we serve it
} else if (exists) {
listener(path);
// Else, we download the image and notify everyone when done
} else {
addListener(uri, listener);
await FileSystem.downloadAsync(uri, path);
notifyAll(uri, path);
unsubscribe(uri);
}
}

Progressive image loading

Every-time an image URI is stored in the database, store it’s base64 preview with it. This will allow you to load the image super smoothly. Below is an example:

{
preview: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
uri: "https://firebasestorage.googleapis.com/v0/b/react-native-e.appspot.com/o/b47b03a1e22e3f1fd884b5252de1e64a06a14126.png?alt=media&token=d636c423-3d94-440f-90c1-57c4de921641"
}

Now you can immediately display a blurred version of the preview and decrease the blur to 0 when the full version of the image is loaded. On iOS, the <BlurView> from ExpoKit conveniently supports the native animation driver. An Android however, it doesn't default to an animated opacity view. I made a pull request to BlurView.android.js in order to test a water and find out if the team would be open to have an implementation of <BlurView> that would be more symmetrical on both platforms. In the meantime you can implement your own. This is how it looks like in react-native-expo-image-cache:

// intensity is an Animated.Value
const opacity = intensity.interpolate({
inputRange: [0, 100],
outputRange: [0, 1]
});
{
Platform.OS === "ios" && (
<AnimatedBlurView
tint="dark"
style={computedStyle}
{...{intensity}}
/>
)
}
{
Platform.OS === "android" && (
<Animated.View
style={[computedStyle, { backgroundColor: "rgba(0, 0, 0, 0.5)", opacity }]}
/>
)
}

There is a serious bug in <Image>

This one. You might be tempted to set a URI in the state of your image component: set it to the preview data URI and then to the full image when loaded. Problem is, sometimes the image won’t refresh. You can use key to force its refresh but it will flicker. Solution? Superpose the full image to the preview. Again, this is how it looks like in react-native-expo-image-cache:

{
// If show the preview if it exists
hasPreview && (
<RNImage
source={{ uri: preview }}
resizeMode="cover"
style={computedStyle}
/>
)
}
{
// If the image is loaded, we show it on top
// this.onLoadEnd is used to start the deblurring animation
(uri && uri !== preview) && (
<RNImage
source={{ uri }}
resizeMode="cover"
style={computedStyle}
onLoadEnd={this.onLoadEnd}
/>
)
}

That’s all Folks!

We will never finish to talk about Images in React Native. This is my top 5 list. Am I missing some items? Please let know and in the meantime, Happy Hacking 🎉

Looking for beautiful UI kits? I’m implementing all screens and components from Sketch Elements in React Native. You can get it here. I’m also posting live coding sessions and tutorials on my Youtube channel.