Smart Images in React Native

Home / Developer Tools / Smart Images in React Native
Smart Images in React Native

Update. In order to avoid common pitfalls when dealing with React Native images, you should also checkout the story below. All these recipes are available in the react-native-expo-image-cache npm package.

Images & React Native is a delicate topic. In a recent project, I needed to cache image efficiently as well as displaying them using a progressive image loading technic. On top of that, when a user is uploading a picture, it was required to upload a preview of the picture alongside in order to provide a progressive loading effect for that picture.

Progressive image loading in the Fiber starter kit

This story contains the following the recipes:

  • Progressive image loading
  • Building picture previews client-side before upload
  • Cache images

Progressive Image Loading

In react web, progressive image loading is fairly trivial. There are many packages available for that task and I wrote my own. In the native world, things are a bit more tricky. You need to find a component that can provide a blur effect to your image as well as being able to provide motion to the blur effect using the Animated API. Expo provides the BlurView component which only works on iOS but can easily be animated. There is another componentthat has native dependencies but provides support for Android and cannot be animated via the Animated API at the moment. Below is a simple implementation of a ProgressiveImage component. We first show the preview, use Image.prefetch to loading the image, and then decrease the blur intensity once the image is ready.

// @flow
import * as React from "react";
import {Image, Animated, StyleSheet, View, Platform} from "react-native";
import {BlurView} from "expo";
import type { StyleObj as Style } from "react-native/Libraries/StyleSheet/StyleSheetTypes";

type ProgressiveImageProps = {
    style: Style,
    preview: string,
    uri: string
};

type ProgressiveImageState = {
    uri: string,
    intensity: Animated.Value
};

const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
export default class ProgressiveImage extends React.Component<ProgressiveImageProps, ProgressiveImageState> {

    async componentWillMount(): Promise<void> {
        const {preview, uri} = this.props;
        this.setState({ uri: preview, intensity: new Animated.Value(100) });
        await Image.prefetch(uri);
        this.setState({ uri });
    }

    onLoadEnd(uri: string) {
        const {preview} = this.props;
        const isPreview = uri === preview;
        if (isPreview && Platform.OS === "ios") {
            const intensity = new Animated.Value(100);
            this.setState({ intensity });
            Animated.timing(intensity, { duration: 300, toValue: 0, useNativeDriver: true }).start();
        }
    }

    render(): React.Node {
        const {style} = this.props;
        const {uri, intensity} = this.state;
        const computedStyle = [
            StyleSheet.absoluteFill,
            style
        ];
        return (
            <View {...{style}}>
                {
                    uri && (
                        <Image
                            source={{ uri }}
                            resizeMode="cover"
                            style={computedStyle}
                            onLoadEnd={() => this.onLoadEnd(uri)}
                        />
                    )
                }
                {
                    Platform.OS === "ios" && <AnimatedBlurView tint="default" style={computedStyle} {...{intensity}} />
                }
            </View>
        );
    }
}

You can play with the component above here.

Example of a simple expo snack for image loading.

Build Preview Before Upload

To provide a progressive image loading effect to a picture, you need a small preview of that picture. This preview can easily be generated server-side. You can also build this preview client-side using ImageEditor. Why do it client-side? It allows you to provide an upload feature to your app without writing any server-side code. You build the preview on the client and uploading both picture to a storage as a service provider such as S3 or Firebase. The code snippet below builds a 50×50 preview from a user provided image.

const previewParams = (width: number, height: number) => ({
    offset: {
        x: 0,
        y: 0
    },
    size: {
        width,
        height
    },
    displaySize: {
        width: 50,
        height: 50
    },
    resizeMode: "cover"
});

const buildPreview({ uri, width, height }: Picture): Promise<string> {
      return new Promise((resolve, reject) =>
          ImageEditor.cropImage(
              picture.uri,
              previewParams(picture.width, picture.height),
              uri => ImageStore.getBase64ForTag(
                  uri, data => resolve(`data:image/jpeg;base64,${data}`), err => reject(err)
              ),
              err => reject(err)
          )
      );
  }

Caching Images

In some cases, 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 from ting, you see the difference from using the default Image cache versus serving the image locally.

Without cache on the left and with cache on the right

The cache function is very simple, we create a unique local path based on the hash of the file URI. If the image exists, we return its local path. If it doesn’t exist, we download it and return the local path.

// @flow
import SHA1 from "crypto-js/sha1";
import { FileSystem } from "expo";

export const cacheImage = async(uri: string): Promise<string> => {
    const ext = uri.substring(uri.lastIndexOf("."), uri.indexOf("?") === -1 ? undefined : uri.indexOf("?"));
    const path = FileSystem.cacheDirectory + SHA1(uri) + ext;
    const info = await FileSystem.getInfoAsync(path);
    if (!info.exists) {
        await FileSystem.downloadAsync(uri, path);
    }
    return path;
};

Putting it all together

I ended up writing a single Image component that provides the progressive image loading technic et the caching strategy described above. The standalone implementation can be seen here.

Source: hackernoon