avatarGregoire Imber

Summary

The context discusses a custom solution for implementing image caching in a React Native Expo app to improve app performance and user experience.

Abstract

The context presents a detailed guide on how to create a custom component for image caching in a React Native Expo app. It explains the importance of image caching for enhancing app performance and user experience, as well as the challenges faced when using existing solutions like React Native Fast Image and React Native Cached Image. The author shares their custom component code and explains how it works, including the use of the FileSystem and Crypto libraries for caching images and the Image.getSize function for reducing requests to Firebase. The article also includes a comparison of loading times with and without the custom component, demonstrating its effectiveness in improving app performance.

Bullet points

  • Image caching is crucial for enhancing app performance and user experience.
  • Existing solutions for image caching in React Native Expo apps can be challenging to implement.
  • The author shares their custom component code for image caching in a React Native Expo app.
  • The custom component uses the FileSystem and Crypto libraries for caching images.
  • The Image.getSize function is used to reduce requests to Firebase.
  • The custom component improves app performance by reducing loading times.
  • A comparison of loading times with and without the custom component is included.

Expo App Image Caching: Custom Component Solution

Photo by Compare Fibre on Unsplash

Creating an app that simply just works nowadays is not enough. As technology improves, users (including myself!) have a shorter attention span and patience for when it comes to loading times. Having a seamless and responsive user experience is paramount. One way that loading times can be drastically reduced in your app is by optimising image loading through the caching of images.

What is Image Caching and why implement it?

Image Caching is a crucial technique in both mobile and web app development for enhancing the performance of your app. It is essentially the process of storing pictures in temporary memory locally on the user’s device. This allows your app to display images quickly, even when the user has sub-par network conditions. Image Caching also eliminates the need for repeated downloads of the same image which not only can be time consuming and frustrating for the user but also reduces the amount of data transferred over the network. This can save you big time on bandwidth costs, making your app more cost-efficient.

The biggest pro, however, to using image caching is the overall improvement to the user experience, which will enhance satisfaction and therefore increase the retention of your users. When users see images promptly, they are more likely to engage with your app and return for more.

Implementing Image Caching in React Native

There are a few image caching components available on GitHub for React Native – some notable ones are:

  1. React Native Fast Image by DylanVann
  2. React Native Cached Image by Kfiroo

Both of these give great details of control over the caching in your react native app, however, I had some difficulties implementing these into my Expo App and I did not need the extra control so I decided to go down the simple custom solution route.

Custom Solution

The advantage of creating your own component for image caching is that you can customise it to exactly suit your own needs. If done correctly, this can remove unnecessary complications and increase efficiency. I also wanted to learn how image caching works instead of just installing a package; I feel like you learn so much more about the ‘nitty gritty’ when you do it yourself.

After some research, I found a package which gave a good barebones starting place for the custom component. The code can be found in this article: https://blog.boot.dev/javascript/how-to-cache-images-react-native-expo-managed/ by Lane (wagslane).

I have inserted my custom component below:

import React, { Component } from "react";
import {
  ActivityIndicator,
  Dimensions,
  Image,
  ImageLoadEventData,
  ImageStyle,
  NativeSyntheticEvent,
} from "react-native";
import * as FileSystem from "expo-file-system";
import * as Crypto from "expo-crypto";

export default class CachedImage extends Component<
  {
    source: { uri: string };
    desiredHeight?: number;
    style?: ImageStyle;
    onLoad?: (event: NativeSyntheticEvent<ImageLoadEventData>) => void;
  },
  {
    imgURI: string | undefined;
    desiredWidth: number;
    loading: boolean;
    desiredHeight: number;
  }
> {
  constructor(props: any) {
    super(props);
    this.state = {
      imgURI: undefined,
      desiredWidth: 0,
      loading: false,
      desiredHeight: this.props.desiredHeight ?? 0,
    };
  }

  async componentDidMount() {
    this.setState({ loading: true });
    const filesystemURI = await this.getImageFilesystemKey(
      this.props.source.uri
    );
    await this.loadImage(filesystemURI, this.props.source.uri);
    this.setState({ loading: false });
  }

  async componentDidUpdate() {
    const filesystemURI = await this.getImageFilesystemKey(
      this.props.source.uri
    );
    if (
      this.props.source.uri === this.state.imgURI ||
      filesystemURI === this.state.imgURI
    ) {
      return null;
    }
    await this.loadImage(filesystemURI, this.props.source.uri);
  }

  async getImageFilesystemKey(remoteURI: string) {
    const hashed = await Crypto.digestStringAsync(
      Crypto.CryptoDigestAlgorithm.SHA256,
      remoteURI
    );
    return `${FileSystem.cacheDirectory}${hashed}`;
  }

  async loadImage(filesystemURI: string, remoteURI: string) {
    try {
      // Use the cached image if it exists
      const metadata = await FileSystem.getInfoAsync(filesystemURI);
      if (metadata.exists) {
        this.setState({
          imgURI: filesystemURI,
        });

        if (!!this.props.desiredHeight) {
          const screenWidth = Dimensions.get("screen").width;

          Image.getSize(filesystemURI, (width, height) => {
            // If the desired heigh would make image too wide for the screen
            // Make the image 100% width instead
            if (
              !!this.props.desiredHeight &&
              (this.props.desiredHeight / height) * width > screenWidth
            ) {
              const newWidth = screenWidth;
              const newHeight = (screenWidth / width) * height;

              this.setState({
                desiredWidth: newWidth,
                desiredHeight: newHeight,
              });
            } else {
              this.setState({
                desiredWidth: this.props.desiredHeight
                  ? (this.props.desiredHeight / height) * width
                  : 0,
              });
            }
          });
        }

        return;
      }

      // otherwise download to cache
      const imageObject = await FileSystem.downloadAsync(
        remoteURI,
        filesystemURI
      );
      this.setState({
        imgURI: imageObject.uri,
      });

      if (!!this.props.desiredHeight) {
        const screenWidth = Dimensions.get("screen").width;

        Image.getSize(imageObject.uri, (width, height) => {
          if (
            !!this.props.desiredHeight &&
            (this.props.desiredHeight / height) * width > screenWidth
          ) {
            const newWidth = screenWidth;
            const newHeight = (screenWidth / width) * height;

            this.setState({
              desiredWidth: newWidth,
              desiredHeight: newHeight,
            });
          } else {
            this.setState({
              desiredWidth: this.props.desiredHeight
                ? (this.props.desiredHeight / height) * width
                : 0,
            });
          }
        });
      }
    } catch (err) {
      console.log("Image loading error:", err);
      this.setState({ imgURI: remoteURI });
    }
  }

  render() {
    return this.state.loading ? (
      <ActivityIndicator
        style={{
          flex: 1,
          width: !!this.props.style?.width
            ? this.props.style?.width
            : this.state.desiredWidth,
          height: !!this.props.style?.height
            ? this.props.style?.height
            : this.state.desiredHeight,
          justifyContent: "center",
          alignItems: "center",
        }}
      />
    ) : (
      <Image
        {...this.props}
        style={[
          this.props?.style,
          {
            width: !!this.props.style?.width
              ? this.props.style?.width
              : this.state.desiredWidth,
            height: !!this.props.style?.height
              ? this.props.style?.height
              : this.state.desiredHeight,
          },
        ]}
        source={{ uri: this.state.imgURI ?? undefined }}
      />
    );
  }
}

An example of how this component can then be used is shown below:

<CachedImage
     source={{ uri: uri }}
     style={{
       width: size,
       height: size,
       resizeMode: "cover",
       borderRadius: size / 2,
    }}
  />

Where ‘width’ and ‘height’ are integer values to control the size of the image. Being able to set the desired size of the image is something that I really wanted to implement in my custom component.

I also found that the Image.getSize function was making lots of requests to Firebase (where I am storing my images in my project), causing me to reach my daily quota limit very quickly. Therefore, in the component, I point the Image.getSize function to the cached image instead, drastically reducing the number of requests made to Firebase.

Results

Here is a little comparison in loading times using the CachedImage component vs not using it.

Without using the CachedImage component:

With using the CachedImage component:

Thank you for taking the time to read this article. As always, let me know what you thought and if you have any questions / comments I am happy to answer 💪

I am currently developing my own app and the purpose of writing this article is to help consolidate what I have learnt and share my personal experience with app development along the way. If you would like to see more about my app and its development, feel free to check out my Instagram account: @vroomapp_ and landing page: https://vroomapp.uk/.

Software Development
App Development
React Native
Expo
React
Recommended from ReadMedium