Nitish Sharma I am a mobile and web developer proficient in React, React Native, and other libraries. I am currently employed as a React Native developer.

Caching images in React Native: A tutorial with examples

6 min read 1792

Caching Images in React Native: A Tutorial With Examples

From social media services, to rideshare apps, to blogging platforms, images hold quite an important position for data representation. They play a large role in enhancing the user experience and are indeed vital to the user-friendliness of your app.

From a developer point of view, loading remote images isn’t a huge pain point in React Native. But even with the best of the optimizations added to the Component, be it a class or functional component, image loading and rerendering can slow down the app, which leads a laggy interface.

In this tutorial, we’ll first show you how to cache images in React Native using the react-native-fast-image library. Then, we’ll demonstrate how to build your own React Native image caching component from scratch with step-by-step instructions and detailed examples.

Here’s what we’ll cover:

To follow along, you should be familiar with the basics of React Native — e.g., JSX, components (class as well as functional), and styling. You can simply copy and paste the code blocks from this guide, but I would suggest reading through the whole tutorial for better understanding.

What is image caching in React Native?

Caching is a great way to solve issues associated with loading and rerendering images from remote endpoints. Image caching essentially means downloading an image to the local storage in the app’s cache directory (or any other directory that is accessible to the app) and loading it from local storage next time the image loads.

There are a few ways to approach image caching in React Native. If you’re building a bare-bones React Native app, there’s a wonderful component available that handles all your image caching automatically without writing any extra code called React Native FastImage. Or, if you’re using Expo or working on a more complex project, you might decide to build your own image caching component from scratch.

What is react-native-fast-image?

react-native-fast-image is a performant React Native component for loading images. FastImage aggressively caches all loaded images. You can add your own request auth headers and preload images. react-native-fast-image even has GIF caching support.

To start using React Native FastImage, first import the FastImage component:

import FastImage from 'react-native-fast-image';

Below is the basic implementation of the FastImage component:

We made a custom demo for .
No really. Click here to check it out.

<FastImage
  style={{ width: 200, height: 200 }}
  source={{uri: 'https://unsplash.it/400/400?image=1'}}
/>

Here’s a preview of what this looks like:

react-native-fast-image Example

Using the FastImage component: A practical example

Let’s look at a basic example of using the FastImage component with a few props:

<FastImage
  style={{ width: 200, height: 200 }}
  source={{
      uri: '...image...url...',
      headers: { Authorization: 'someAuthToken' },
      priority: FastImage.priority.normal,
  }}
  resizeMode={FastImage.resizeMode.contain}
/>

As you can see, this example is almost the same as the basic React Native image component, but on steroids. Let’s break down the code in finer detail.

  • source contains the image, its headers, and more
  • uri represents the path of the image you want to load
  • headers represent the headers you might need (auth token in the example above)
  • priority signifies the priority of the images — e.g., if you need to load a certain image first, you can set the priority to FastImage.priority.high

React native cache property

cache is where things get exciting. You’re probably familiar with uri, header, and others props of the Image component. It’s the same for FastImage with only slight changes. cache is what you’d use to change the behavior of image caching and image loading.

There are three properties you can use in cache:

  1. FastImage.cacheControl.immutable is the default property for the FastImage component. The image only caches or updates if the uri is changed
  2. FastImage.cacheControl.web enables you to configure the FastImage component to cache images like the browser, using headers and the normal caching procedure
  3. FastImage.cacheControl.cacheOnly enables you to restrict the FastImage component to fetch from already-cached images — i.e., without making any new network requests.

Here’s an example of an image with the cache property:

<FastImage
  style={{ width: 200, height: 200 }}
  source={{
    uri: 'https://unsplash.it/400/400?image=1',
    cache: FastImage.cacheControl.cacheOnly
  }}
/>

To state the benefit simply, if you can maintain a local database of images that are loaded once, you can us this cache property to save on bandwidth costs by fetching cached images from device storage.

How to build an image caching component from scratch

FastImage is great for bare-bones React Native projects, but if you’re using Expo or have needs that react-native-fast-image can’t meet, you may want to write your own image caching component.

Before building your own image caching component, it’s crucial to understand the basics of caching an image. Let’s review: To cache an image is to store it in the local storage of the device so that it can be accessed quickly next time around without any network requests.

To to cache an image, we need the network URI, or URL of that image, and a string identifier to fetch it the next time around. We need a unique identifier for each resource because multiple images can have the same name, which can be a problem when differentiating between the local cache and images with redundant names.

For this guide, I’ll assume that you’re either building your app using expo or using expo-file-system via unimodules in bare React Native.

Our component should take in three basic props:

  1. source for the URI of the network image
  2. cacheKey for the unique identifier for an image
  3. style for styling the image component:
    let image = {
        uri: "https://post.medicalnewstoday.com/wp-content/uploads/sites/3/2020/02/322868_1100-800x825.jpg",
        id: "MFdcdcdjvCDCcnh", //the unique id that you can store in your local db
      };
    <CustomFastImage
      source={{ uri: image.uri }}
      cacheKey={image.id}
      style={{ width: 200, height: 200 }}
    />

For the logic of our custom image caching component, we’ll import expo-file-system:

import * as FileSystem from "expo-file-system";

First, we need to create a new local path for our remote image using the cacheKey (unique ID) to check whether it already exists in the local cache and, if not, download it.

We need to initialize the props we’re going to receive:

const CustomFastImage = (props) => {
  const {
    source: { uri },
    cacheKey,
    style,
  } = props;
...

And the function to get the extension of the image from uri:

function getImgXtension(uri) {
  var basename = uri.split(/[\\/]/).pop();
  return /[.]/.exec(basename) ? /[^.]+$/.exec(basename) : undefined;
}

This function returns an array of extensions. You can just use the first item of the array.

Then, we’ll call this function to get the extension from the useEffect Hook from the component and use the returned extension to create the local cache path for the image:

useEffect(() => {
    async function loadImg() {
      let imgXt = getImgXtension(uri);
      if (!imgXt || !imgXt.length) {
        Alert.alert(`Couldn't load Image!`);
        return;
      }
      const cacheFileUri = `${FileSystem.cacheDirectory}${cacheKey}.${imgXt[0]}`;
    }
    loadImg();
  }, []);

FileSystem.cacheDirectory is the path of the cache directory. You can change this according to your own preference.

Now, we need to check whether the image at this path already exists using a function like this:

async function findImageInCache(uri) {
  try {
    let info = await FileSystem.getInfoAsync(uri);
    return { ...info, err: false };
  } catch (error) {
    return {
      exists: false,
      err: true,
      msg: error,
    };
  }
}

Add this to useEffect > loadImg():

useEffect(() => {
    async function loadImg() {
      let imgXt = getImgXtension(uri);
      if (!imgXt || !imgXt.length) {
        Alert.alert(`Couldn't load Image!`);
        return;
      }
      const cacheFileUri = `${FileSystem.cacheDirectory}${cacheKey}.${imgXt[0]}`;
      let imgXistsInCache = await findImageInCache(cacheFileUri);
    }
    loadImg();
  }, []);

Now we need a function to cache the image to local storage if it is not already cached and return the desired output:

async function cacheImage(uri, cacheUri, callback) {
  try {
    const downloadImage = FileSystem.createDownloadResumable(
      uri,
      cacheUri,
      {},
      callback
    );
    const downloaded = await downloadImage.downloadAsync();
    return {
      cached: true,
      err: false,
      path: downloaded.uri,
    };
  } catch (error) {
    return {
      cached: false,
      err: true,
      msg: error,
    };
  }
}

We’ll also need a const with the useState() Hook to store the path of the image once loaded:

const [imgUri, setUri] = useState("");

For a better user experience, you can add an ActivityIndicator (or any loading indicator of that sort according to your preference) and implement it according to the change in the imgUri state.

return (
    <>
      {imgUri ? (
        <Image source={{ uri: imgUri }} style={style} />
      ) : (
        <View
          style={{ ...style, alignItems: "center", justifyContent: "center" }}
        >
          <ActivityIndicator size={33} />
        </View>
      )}
  </>
);

In the useEffect Hook, we need to update the imgUri when the image is cached or already available in the local storage:

  useEffect(() => {
    async function loadImg() {
      let imgXt = getImgXtension(uri);
      if (!imgXt || !imgXt.length) {
        Alert.alert(`Couldn't load Image!`);
        return;
      }
      const cacheFileUri = `${FileSystem.cacheDirectory}${cacheKey}.${imgXt[0]}`;
      let imgXistsInCache = await findImageInCache(cacheFileUri);
      if (imgXistsInCache.exists) {
        console.log("cached!");
        setUri(cacheFileUri);
      } else {
        let cached = await cacheImage(uri, cacheFileUri, () => {});
        if (cached.cached) {
          console.log("cached NEw!");
          setUri(cached.path);
        } else {
          Alert.alert(`Couldn't load Image!`);
        }
      }
    }
    loadImg();
  }, []);

Here’s the complete code for the CustomFastImage component we’ve built:

import React, { useEffect, useRef, useState } from "react";
import { Alert, Image, View, ActivityIndicator } from "react-native";
import * as FileSystem from "expo-file-system";
function getImgXtension(uri) {
  var basename = uri.split(/[\\/]/).pop();
  return /[.]/.exec(basename) ? /[^.]+$/.exec(basename) : undefined;
}
async function findImageInCache(uri) {
  try {
    let info = await FileSystem.getInfoAsync(uri);
    return { ...info, err: false };
  } catch (error) {
    return {
      exists: false,
      err: true,
      msg: error,
    };
  }
}
async function cacheImage(uri, cacheUri, callback) {
  try {
    const downloadImage = FileSystem.createDownloadResumable(
      uri,
      cacheUri,
      {},
      callback
    );
    const downloaded = await downloadImage.downloadAsync();
    return {
      cached: true,
      err: false,
      path: downloaded.uri,
    };
  } catch (error) {
    return {
      cached: false,
      err: true,
      msg: error,
    };
  }
}
const CustomFastImage = (props) => {
  const {
    source: { uri },
    cacheKey,
    style,
  } = props;
  const isMounted = useRef(true);
  const [imgUri, setUri] = useState("");
  useEffect(() => {
    async function loadImg() {
      let imgXt = getImgXtension(uri);
      if (!imgXt || !imgXt.length) {
        Alert.alert(`Couldn't load Image!`);
        return;
      }
      const cacheFileUri = `${FileSystem.cacheDirectory}${cacheKey}.${imgXt[0]}`;
      let imgXistsInCache = await findImageInCache(cacheFileUri);
      if (imgXistsInCache.exists) {
        console.log("cached!");
        setUri(cacheFileUri);
      } else {
        let cached = await cacheImage(uri, cacheFileUri, () => {});
        if (cached.cached) {
          console.log("cached NEw!");
          setUri(cached.path);
        } else {
          Alert.alert(`Couldn't load Image!`);
        }
      }
    }
    loadImg();
    return () => (isMounted.current = false);
  }, []);
  return (
    <>
      {imgUri ? (
        <Image source={{ uri: imgUri }} style={style} />
      ) : (
        <View
          style={{ ...style, alignItems: "center", justifyContent: "center" }}
        >
          <ActivityIndicator size={33} />
        </View>
      )}
    </>
  );
};
export default CustomFastImage;

Conclusion

In this tutorial, we covered everything you need to know about image caching in React Native. We went over how to use react-native-fast-image to cache images in React Native as well as how to build your own image caching component from scratch.

For next steps, you might consider adding animations, loading indicators, and other bells and whistles to the component. You could also add a progress indicator or better a callback function using the FileSystem API.

Nitish Sharma I am a mobile and web developer proficient in React, React Native, and other libraries. I am currently employed as a React Native developer.

Leave a Reply