Implement Your Own Image Component With Local Cache for React Native

Teddy MorinTeddy Morin
5 min read

If you are here today, you may want to set up a caching system for images on a React Native application. Let me preface this article by telling you: a custom solution is probably not the solution you’re looking for.

Fast Image

At the time of writing this article, libraries exist, react-native-fast-image being the most popular.

Did you know that the default Image component from react-native is actually supposed to handle caching? Though, you guessed it, it’s not solving all our problems. The documentation of react-native-fast-image explains it clearly:

React Native's Image component handles image caching like browsers for the most part. If the server is returning proper cache control headers for images you'll generally get the sort of built in caching behavior you'd have in a browser. Even so many people have noticed:

  • Flickering.

  • Cache misses.

  • Low performance loading from cache.

  • Low performance in general.

If you have nothing against using a new library, react-native-fast-image will definitely solve your issues.

A custom solution

On the other hand, you might be unable to use an external library.

Maybe you’re using an image delivery service that comes with its own component. You might be afraid of building an image-intensive application on an external solution or are simply looking for a new learning opportunity.

In the second part of this article, you will find a custom solution with code examples and a public repository, in TypeScript.

Requirements

I find “handle caching” to be quite abstract, so let’s define exactly what we want.

To get started, we need a component that takes an image URI, stores it locally the first time it’s displayed, and re-uses it when necessary.

That sounds great, but it implicitly means two things:

  • As we store an image locally, we should be able to invalidate it at some point, and re-download the original.

  • A stored image might be deleted without us knowing about it. If it ever happens, we also need to re-download the original one.

As we are modern React developers, we will separate our code into two files: an Image component and a hook. I’ll call them respectively CachedImage and useCachedImage.

Image component

Most of the logic will be stored inside a hook. Our component simply provides an API similar to the original Image from react-native, and:

  • Pass the original URI to the hook and retrieve the cached path

  • Tell the hook if loading the cached image caused an error

Everything seems pretty straightforward, even the loading error with onError. To make this component similar to the original Image, we can re-use the same props, maybe changing the source for a raw URI:

Hook

The hook takes the original image URI and returns the cached image path, as well as an error callback. It’s supposed to:

  • Download and save the image on disk

  • Store which original image is saved and where

Additionally, we can define an expiration after which images will be re-downloaded. That means, we won’t simply store which original image is saved and where, but add metadata such as its creation date.

We can start our hook and ignore the image download for now, but complete it in a second step. First, let’s define a type for the data we store, and the expiration time:

As you can see, we will use an entry type with the local path of the image and its creation date. Also, we can easily use a constant to determine if an entry is expired (here, 7 days).

We can start with the skeleton of our hook. It will use a state for the local path, which will be retrieved every time the original URI changes. I’ll create an empty getCachedImageEntry function that is responsible for finding an entry based on an original URI and complete it later as well.

As you can see, on a loading error, we simply redownload the original image. It also gets downloaded if no entry is found, or it’s expired. Now, the biggest issues are:

  • Where do we store our entries

  • How do we download an image

My mind is set on react-native-mmkv for entries, and react-native-blob-util for file download. With MMKV, we can use the original URI as key, and the entry as value:

With react-native-blob-util, we have a very straightforward example to download and store a file directly. There are a few elements we need to pay attention to:

  • On Android, we need to add "file://" before the file path.

  • For the Image component to work correctly, we need to add the image extension to the stored file.

That means we have to:

  • Download the image file

  • Extract its extension and add it to the file path

  • Store the entry with the generated path and creation date

  • Call setLocalPath

That’s it, you have a minimal Image component with local cache! Here are a few thoughts for a real-world caching system:

  • You probably don’t want a static expiration time, but based on cache control headers or configurable options.

  • You could potentially get rid of MMKV or equivalent, and use a sanitized path to retrieve images locally.

  • After invalidating an entry, you probably want to delete the related image locally.

  • You could retrieve the initial entry for the localPath initial state instead of waiting for the useEffect.

  • I’m sure there are dozens more…

If you’re looking for a complete repository, have a look here.

0
Subscribe to my newsletter

Read articles from Teddy Morin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Teddy Morin
Teddy Morin

Teddy is a full-stack Software Engineer. He has experience as a Developer, Lead Developer, and teacher. Previously, he had the opportunity to work on projects at scale, in industries like pharmaceutical or e-commerce. His mission here is to provide you with learning materials he didn't have so you don't waste time in your software journey.