Implementing fast-image for react-native expo apps.
Tags: reactnative, hooks, javascript
Published: 2021-01-19 01:49:12

---
title: Implementing fast-image for react-native expo apps.
published: true
description: Here we look how to implement image caching in Expo apps
tags: reactnative, hooks, JS
//cover_image: https://direct_url_to_image.jpg
canonical_url: https://www.echowaves.com/post/implementing-fast-image-for-react-native-expo-apps
---
Original code for this post can be found here: [https://github.com/echowaves/WiSaw/blob/master/src/components/CachedImage/index.js](https://github.com/echowaves/WiSaw/blob/master/src/components/CachedImage/index.js)

The code is used in production on a [What I Saw](https://www.wisaw.com/) mobile app that renders tons of images very fast: [iOS](http://itunes.apple.com/us/app/wisaw/id1299949122), [Android](http://play.google.com/store/apps/details?id=com.echowaves.wisaw)

And the article is a repost from: [https://www.echowaves.com/post/implementing-fast-image-for-react-native-expo-apps](https://www.echowaves.com/post/implementing-fast-image-for-react-native-expo-apps)

Recently this component was extracted into separate npm module [expo-cached-image] (https://www.npmjs.com/package/expo-cached-image)

---------
If you've ever written **react-native** apps which  rely on **react-native-fast-image** npm, you are probably aware that, unfortunately, this wonderful component simply does not work in **react-native** apps developed with **Expo**, because it uses platform specific implementation.

The development community has made numerous requests to **the Expo** team to include support for **fast-image**, unfortunately this is not a priority at this time. This leaves us no options but to implement something ourselves.


Let's call our component **CachedImage**. We will be using the latest react version, which supports function hooks, as they are more efficient than Class based components. And the efficiency -- that's what we are after.


To make it work with Expo, we will be using expo's components that work in iOS and Android out of the box. For instance **FileSystem** from **'expo-file-system'** npm.


We will invoke our component like this:

```javascript
<CachedImage
          source={{ uri: `${item.getThumbUrl}` }}
          cacheKey={`${item.id}t`}
          style={styles.thumbnail}
        />
```

Generally speaking, it works just like a native `<Image/>` with one exception -- it requires **cacheKey** prop.


Now, let's start working on our **CachedImage** component:

First we will declare  **filesystemURI**, which derives from **cacheKey** prop and defines unique cache entry for our image.

```javascript
 const filesystemURI = `${FileSystem.cacheDirectory}${cacheKey}`
```

Then declaring  **imgURI**  -- the state const that we pass to the actual <Image/> tag when we render our component in the return.
```javascript
const [imgURI, setImgURI] = useState(filesystemURI)
```

Note, if the image is not cached yet (on the first run), it will reference non existing file.


To prevent updating component that is unmounted, we will declare:

```javascript
const componentIsMounted = useRef(true)
```

Next, let's implement useEffect which kicks in only once, when the component is mounted:

```javascript
useEffect(() => {
...
    loadImage({ fileURI: filesystemURI })

    return () => {
      componentIsMounted.current = false
    }
  }, [])// eslint-disable-line react-hooks/exhaustive-deps
```

Now let's implement the **loadImage** method -- the meats of our solution.  Here is how it looks:

```javascript
const loadImage = async ({ fileURI }) => {
      try {
        // Use the cached image if it exists
        const metadata = await FileSystem.getInfoAsync(fileURI)
        if (!metadata.exists) {
          // download to cache
          if (componentIsMounted.current) {
            setImgURI(null)
            await FileSystem.downloadAsync(
              uri,
              fileURI
            )
          }
          if (componentIsMounted.current) {
            setImgURI(fileURI)
          }
        }
      } catch (err) {
        console.log() // eslint-disable-line no-console
        setImgURI(uri)
      }
    }
```

Pretty self explanatory. First, check if the file with fileURI exists.  If not, then 
```javascript
setImgURI(null)
```
This will force the Image to render with null source -- perfectly fine, will render empty image. 


After that, download image from the uri  and put it in the cache:
```javascript
 await FileSystem.downloadAsync(
              uri,
              fileURI
            )            
```
And if the component is still mounted (after all that wait), update state via setImage, which will force our component to re-render again:
```javascript
if (componentIsMounted.current) {
            setImgURI(fileURI)
          }
```

Note, that if the file was previously cached, our Image will be already rendering with proper uri  pointing at the file in cache, and this is what makes our solution so fast -- no unnecessary re-renders, no calculations, just render Image straight from cache. If not, we will **await** until the file downloads, prior to updating state with setImageURI to trigger the Image to re-render. Yes, it will have to re-render our component couple of  times, but, since downloading images will be slow anyways, not a really big deal -- as long as we optimize rendering of the image when it's already cached.


And this is how we render our component:
```javascript
  return (
    <Image
    // eslint-disable-next-line react/jsx-props-no-spreading
      {...props}
      source={{
        uri: imgURI,
      }}
    />
  )
```
Can't get any simpler than that. 


It took me some trial and error to find the most efficient combination. Initially, I was trying to avoid using **cacheKey** and calculate the key as crypto hash function -- I found it performing much slower than what I was hoping for. After all, crypto hash  function relies on a heavy math calculations.  As such, I view having to pass **cacheKey** prop as a minor inconvenience, but this approach gives us the best performance possible. All my images already have unique ids, so, why not to use it as **cacheKey**? 


And the complete code for the CachedImage component is down below. Let me know if you can think of any other  optimization improvements:

```javascript
import React, { useEffect, useState, useRef } from 'react'

import { Image } from 'react-native'

import * as FileSystem from 'expo-file-system'

import PropTypes from 'prop-types'

const CachedImage = props => {
  const { source: { uri }, cacheKey } = props
  const filesystemURI = `${FileSystem.cacheDirectory}${cacheKey}`

  const [imgURI, setImgURI] = useState(filesystemURI)

  const componentIsMounted = useRef(true)

  useEffect(() => {
    const loadImage = async ({ fileURI }) => {
      try {
        // Use the cached image if it exists
        const metadata = await FileSystem.getInfoAsync(fileURI)
        if (!metadata.exists) {
          // download to cache
          if (componentIsMounted.current) {
            setImgURI(null)
            await FileSystem.downloadAsync(
              uri,
              fileURI
            )
          }
          if (componentIsMounted.current) {
            setImgURI(fileURI)
          }
        }
      } catch (err) {
        console.log() // eslint-disable-line no-console
        setImgURI(uri)
      }
    }

    loadImage({ fileURI: filesystemURI })

    return () => {
      componentIsMounted.current = false
    }
  }, [])// eslint-disable-line react-hooks/exhaustive-deps

  return (
    <Image
    // eslint-disable-next-line react/jsx-props-no-spreading
      {...props}
      source={{
        uri: imgURI,
      }}
    />
  )
}

CachedImage.propTypes = {
  source: PropTypes.object.isRequired,
  cacheKey: PropTypes.string.isRequired,
}

export default CachedImage
```