Image caching is great to have your images load quicker, giving the feel of a fast app.
We are just going to implement a very simple image caching hook and CustomImage functional component, using expo-file-system and expo-crypto.
To start, install expo-file system and expo-crypto:
expo install expo-file-system expo-crypto
Quick explanation of the pseudocode:
We are just going to: when our image renders, check if we have the image locally, if we do, show it.
If not, download, and save it locally.
That's literally it.
The actual code:
First off, we'll need to ensure we have a folder created that we can store the images in.
To do so, in the root of our app, we'll make sure that there is a folder ready for our images to be stored in.
import { CACHED_IMAGES_FOLDER } from "../../src/config";
import * as FileSystem from "expo-file-system";
...
const createImageCacheFolder = async () => {
const folderInfo =
await FileSystem.getInfoAsync(CACHED_IMAGES_FOLDER);
if (!folderInfo.exists) {
await FileSystem.makeDirectoryAsync(CACHED_IMAGES_FOLDER);
}
};
Alright, so we got a place to store our images.
Now let's setup a hook that will update our Images uri if the image exists locally (I named it useImageCache):
import { useEffect, useState } from "react";
import * as Crypto from "expo-crypto";
import * as FileSystem from "expo-file-system";
import { CACHED_IMAGES_FOLDER } from "../config";
const useImageCache = sourceUri => {
const [uri, setUri] = useState({ uri: sourceUri });
const fetchImage = async () => {
const digest = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
sourceUri
);
const output = `${CACHED_IMAGES_FOLDER}/${digest}`;
const localFile = await FileSystem.getInfoAsync(output);
if (localFile.exists) {
setUri({ uri: localFile.uri });
return;
}
await FileSystem.downloadAsync(sourceUri, output);
};
useEffect(() => {
fetchImage();
}, [sourceUri]);
return [uri];
};
export default useImageCache;
As you can see, we are checking if the file is in our system, and if so, we'll render it (really quickly).
What's the Crypto stuff doing?
Well, the idea is that creating a hash from the uri, and storing the downloaded image under that hash, instead of storing it directly under the uri itself, will improve performance. Not going to go too much into it (and maybe I'm even wrong on this), but as far as I can tell it probably would be fine to just use the uri instead.
SHA256 seems like a good option because it's not expected to ever produce the same hash from two different uris.
Now onto our CustomImage component:
import React, { memo } from "react";
import { Image } from "react-native";
import { useImageCache } from "../../../src/hooks";
const CustomImage = memo(props => {
if (props.source.uri && !props.shouldNotCache) {
const [uri] = useImageCache(props.source.uri);
return <Image {...props} source={uri} />;
}
return <Image {...props} />;
});
CustomImage.defaultProps = {
source: require("../../../assets/images/icon.png"),
};
export default CustomImage;
I also have memo in there to improve performance, again, not going to focus on it too much. Same with the shouldNotCache prop, which I put in there so I can easily just pass shouldNotCache and the image won't be cached.
Basically what we are doing here is just passing the URI to our hook, and the hook will either update the component with a local uri, or will remain on the initial one.
Actual instance:
<CustomImage
source={{ uri: '[https://reactnative.dev/img/tiny_logo.png](https://reactnative.dev/img/tiny_logo.png)' }}
/>
Admittedly, we may need some styling here for the image to show up, but you can see the actual instances can be very simple.
Overall, it's add a hook, a few lines to create the folder, and you can use caching everywhere with the CustomImage component dropping in for the React Native Image.
There are lots of people smarter than me. Let me know if this was useful to you, or if you see improvements to my code.
I was inspired heavily by https://github.com/jdrouet/expo-image-cache-example, feel free to check out his image caching implementation as well.