How to retry image when error occurs in React?
Tags: react, javascript, typescript, image
Published: 2022-01-21 12:22:41

## Problem

I am trying to load an image and in my case image is hosted in AWS S3 bucket, for some strange reason, image is not available immediately for client to consume and was experiencing a broken thumbnail image in frontend.

Image is getting ready after couple of seconds. So in this case, I wanted to retry image url again until it’s available. 

I am using onerror event handler on this purpose, which gets called when error occurs while loading or rendering an image.

### Step 1:
First I wanted to keep track number of retries. For this I created a useRef variable.

```
const componentRef = useRef<number>();
```

### Step 2:
Initialise ref variable in first render. This is done in useEffect hook.

```
useEffect(() => {
    componentRef.current = RETRY_COUNT;
}, []);
```

### Step 3:
Add onError handler in img tag.

```
<Image src={s3Url} alt={filename} onError={handleError} />
```

### Step 4:
Set error state, Set image source and decrease retry count. 
```
 const handleError = useCallback(({ currentTarget }) => {
    
    setError(true);
    if (componentRef && componentRef.current && componentRef.current > 0) {
      setTimeout(() => {
        currentTarget.onerror = null;
        currentTarget.src = s3Url;
        componentRef.current =
          componentRef && componentRef.current && componentRef.current - 1;
      }, RETRY_DELAY);
    }
  }, []);
```
I have wrapped this logic in setTimeout with 1 second delay, so that this retry will happen in each second.

As a final step to improve UX, you may show loading indicator  while image is being fetched. For that you could create a state variable as below.

```
const [error, setError] = useState<boolean>(false);
```

Set error state
```
const handleError = useCallback(({ currentTarget }) => {
    setError(true);
});
```

Add onload handler in image tag.

```
<Image onLoad={handleLoad} />
```

Set error state false.

```
const handleLoad = useCallback(() => {
    setError(false);
  }, []);

```

Show loading indicator
```
if (error) {
     return <Loading />
}
```


### And finally, here is the full source code.

```
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState
} from "react";
import styled from "styled-components";
import { Loading } from "./loading";

const RETRY_COUNT = 5;
const RETRY_DELAY = 1000;

const Image = styled.img`
  width: 100%;
  height: 100%;
  border-radius: 6px;
`;

interface ImagePreviewProps {
  filename: string;
  s3Url: string;
}

export const ImagePreview: React.FC<ImagePreviewProps> = ({
  filename,
  s3Url,
  loading
}) => {

  const componentRef = useRef<number>();
  const [error, setError] = useState<boolean>(false);

  useEffect(() => {
    componentRef.current = RETRY_COUNT;
  }, []);

  const handleError = useCallback(({ currentTarget }) => {
    setError(true);
    if (componentRef && componentRef.current && componentRef.current > 0) {
      setTimeout(() => {
        currentTarget.onerror = null;
        currentTarget.src = s3Url;
        componentRef.current =
          componentRef && componentRef.current && componentRef.current - 1;
      }, RETRY_DELAY);
    }
  }, []);

  const handleLoad = useCallback(() => {
    setError(false);
  }, []);

  const showImage = useMemo(() => {
    return (
          <Image
            src={s3Url}
            alt={filename}
            onError={handleError}
            onLoad={handleLoad}
          />
        )
    );
  }, [loading, filename, s3Url]);

  if (error) {
     return <Loading />
  }

  return showImage;
};
```