Caching images in React Native: A tutorial with examples
From social media services, to ride share 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:
- What is image caching in React Native?
- What is
react-native-fast-image
? - Using the
FastImage
component: A practical example - React Native
cache
property - How to build an image caching component from scratch
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](https://github.com/DylanVann/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:
<FastImage
style={{ width: 200, height: 200 }}
source={{uri: 'https://unsplash.it/400/400?image=1'}}
/>
Here’s a preview of what this looks like:
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 moreuri
represents the path of the image you want to loadheaders
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 toFastImage.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
:
FastImage.cacheControl.immutable
is the default property for theFastImage
component. The image only caches or updates if theuri
is changedFastImage.cacheControl.web
enables you to configure theFastImage
component to cache images like the browser, using headers and the normal caching procedureFastImage.cacheControl.cacheOnly
enables you to restrict theFastImage
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 need 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 same 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:
-
source
for the uri of the network image -
cacheKey
for the unique identifier for an image -
style
for styling the image componentlet 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](https://blog.logrocket.com/guide-to-react-useeffect-hook/)
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 the 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;
[H2] Other means of image caching in React Native
We have gone through the two methods of caching images in React Native, but, there are other ways for caching, I mean it’s programming, you can build your own means of doing stuff, but we are going to discuss two more methods, that allow us to cache images in a React Native app.
[H3] React Native’s inbuilt image c****omponent
Most new developers miss out on the functionalities that React Native provides by default. One of those functionalities is caching images using the prefetch()
method of the Image
component. Prefetch
, as the name suggests, fetches the image from the remote server and stores it in the local device’s storage for faster loads. The basic usage of prefetch
is:
await Image.prefetch(URL_OF_AN_IMAGE)
For using this method, you might need to either add a placeholder, build a lambda condition, or build a custom component using both of these to make the user experience smooth. The key is to load the image using async/await before showing it in the renderer.
You can check out all the props and methods of the Image
component here.
[H3] ******react-native-cached-image**
This is another way of caching images in React Native. It basically uses a provider, i.e., ImageCacheProvider
, to which we add an array of image URLs that need to be cached by the app. The CachedImage
component is used to display the image that was cached using the ImageCacheProvider
. We can see the implementation below:
const imgs = [url1, url2, url3, ...];
const ImgExample = () => {
return (
<ImageCacheProvider
urlsToPreload={imgs}
onPreloadComplete={() => {
console.log('You can use this callback to show the CachedImage component.');
})
>
<CachedImage source={{uri: imgs[0]}}/>
<CachedImage source={{uri: imgs[1]}}/>
<CachedImage source={{uri: imgs[2]}}/>
</ImageCacheProvider>
);
}
This module also contains ImageCacheManager
, which can be used to delete the image from the cache using various methods available. You can check out the whole module here.
N*.B., the last update of this components was released in 2017, which tends to make a module unreliable. U**se* with caution.
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
.
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.
You can also checkout this post over Logrocket here.