DEV Community

Cover image for Using the FileReader API to preview images in React
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Using the FileReader API to preview images in React

Written by Joseph Mawa✏️

Introduction

Images make up a significant proportion of data transmitted on the internet. More often than not, clients have to upload image files from their devices to the server. To ensure users upload image files of the correct type, quality, and size, most web applications have features for previewing images.

In the browser environment, clients can initiate image upload by browsing files using an input element or the drag and drop API. You can then use the URL API or the FileReader API to read the image files and preview them.

Though previewing images with the URL API is straightforward, using the FileReader API can be daunting. Therefore, in this article, you will learn how to preview images in a React application with the FileReader API. We shall cover both single and batch image previews.

Contents

  • How to browse image files in React
  • Introduction to the FileReader API
  • How to preview single image before upload in React with the FileReader API
  • How to preview multiple images before upload in React with the FileReader API
  • Conclusion

How to browse image files in React

If you want to add file upload functionality to your web application, an input element of type file comes in handy. It enables users to select single or multiple files from storage in their computer or mobile device:

<input type="file" accept="image/*" multiple />
Enter fullscreen mode Exit fullscreen mode

The above input element will look like a button when rendered by the browser. Clicking it will open the operating system's built-in file chooser dialog. The user can then select the image files for upload.

The input element has the accept attribute for restricting the file type. Its value is a string consisting of file type specifiers separated by commas. The value of the accept attribute is image/* in the input element above. It enables us to browse and upload images of any format.

To upload image files of a specific format, you can restrict the value of the accept attribute. For example, setting its value to image/png or .png only accepts PNG images.

With the multiple boolean attribute set to true, a user can select multiple image files. On the other hand, a user can browse only one image file if its value is false. It is worth noting that a boolean attribute's value is true if the attribute is present on an element, and false if omitted.

The browser emits the change event after a user completes the file selection. Therefore, you should listen for the change event on the input element. You can do it like so in React:

<form>
  <p>
    <label htmlFor="file">Upload images</label>
    <input
      type="file"
      id="file"
      onChange={changeHandler}
      accept="image/*"
      multiple
    />
  </p>
</form>
Enter fullscreen mode Exit fullscreen mode

In the change event handler, you can access the FileList object. It is an iterable whose entries are File objects. The File objects contain read-only metadata such as the file name, type, and size:

const changeHandler = (e) => {
  const { files } = e.target
  for (let i = 0; i < files.length; i++) {
    const file = files[i]; // OR const file = files.item(i);
  }
}
Enter fullscreen mode Exit fullscreen mode

Introduction to the FileReader API

The FileReader API provides an interface for asynchronously reading the contents of a file from a web application.

As highlighted in the previous section, you can use an input element of type file to browse files from a user's computer or mobile device. Selecting image files this way returns a FileList object whose entries are File objects.

The FileReader API then uses the File object to asynchronously read the file the user has selected. It is worth mentioning that you can not use the FileReader API to read the contents of a file from the user's file system using the file's pathname.

The FileReader API has several asynchronous instance methods for performing read operations. These methods include:

  • readAsArrayBuffer
  • readAsBinaryString
  • readAsDataURL
  • readAsText

In this article, we shall use the readAsDataURL method. The readAsDataURL method takes the file object as an argument, and asynchronously reads the image file into memory as data URL.

It emits the change event after completing the read operation:

const fileReader = new FileReader();

fileReader.onchange = (e) => {
   const { result } = e.target;
}

fileReader.readAsDataURL(fileObject);
Enter fullscreen mode Exit fullscreen mode

You can read the documentation for a detailed explanation of the other FileReader instance methods.

How to preview single image before upload in React

In this section, we shall look at how to preview a single image before uploading in React with the FileReader API. It assumes you have a React project set up already.

The code below shows how to read and preview a single image in React with the FileReader API. We are using an input element of type file to browse image files. Because we want to preview a single image, I have omitted the multiple boolean attribute on the input element:

import { useEffect, useState } from 'react';

const imageMimeType = /image\/(png|jpg|jpeg)/i;

function App() {
  const [file, setFile] = useState(null);
  const [fileDataURL, setFileDataURL] = useState(null);

  const changeHandler = (e) => {
    const file = e.target.files[0];
    if (!file.type.match(imageMimeType)) {
      alert("Image mime type is not valid");
      return;
    }
    setFile(file);
  }
  useEffect(() => {
    let fileReader, isCancel = false;
    if (file) {
      fileReader = new FileReader();
      fileReader.onload = (e) => {
        const { result } = e.target;
        if (result && !isCancel) {
          setFileDataURL(result)
        }
      }
      fileReader.readAsDataURL(file);
    }
    return () => {
      isCancel = true;
      if (fileReader && fileReader.readyState === 1) {
        fileReader.abort();
      }
    }

  }, [file]);

  return (
    <>
      <form>
        <p>
          <label htmlFor='image'> Browse images  </label>
          <input
            type="file"
            id='image'
            accept='.png, .jpg, .jpeg'
            onChange={changeHandler}
          />
        </p>
        <p>
          <input type="submit" label="Upload" />
        </p>
      </form>
      {fileDataURL ?
        <p className="img-preview-wrapper">
          {
            <img src={fileDataURL} alt="preview" />
          }
        </p> : null}
    </>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

As illustrated in the above example, you can listen for the change event on the input element. The change event handler is invoked after a client completes the file selection. You can access the File object representing the selected file and update state in the event handler.

Since the HTML markup in the browser is editable, it is necessary to check the MIME type of the selected file before starting the read process. Though it is unlikely that an ordinary user would edit the HTML elements on a web page, it prevents anyone from easily breaking your app.

After uploading your files, you will have to do a similar check on the server side. At this point, you can also check the size of the selected file to make sure it does not exceed a maximum limit.

Since reading the selected file is a side effect, we use the useEffect hook. As highlighted in the previous section, you start by creating an instance of FileReader. The readAsDataURL method of the FileReader API reads the file asynchronously and emits the load event after completing the reading process.

It is possible for the component to unmount or rerender before completing the read process. You will need to abort before unmounting if the read process is incomplete. To prevent memory leaks, React disallows state updates after unmounting a component. Therefore, we need to check whether the component is still mounted before updating state in the load event handler.

We access the file's data as a base64-encoded string and update the state after completing the read process. After that, you can render the image preview. For simplicity, I have not added any styling to the form element in the above example.

How to preview multiple images before upload in React

In this section, we shall look at how to preview multiple images before uploading in React with the FileReader API. Like the previous section, it assumes you have a React project set up already.

Reading and previewing multiple images is similar to previewing a single image. We shall modify the code in the previous section slightly. To browse and select several image files, you need to set the value of the multiple boolean attribute to true on the input element.

One noticeable difference is that we are looping through the FileList object in the useEffect Hook and reading the contents of all the selected files before updating the state. We are storing the data URL of each image file in an array, and updating state after reading the last file.

The code below is a modification of the previous example for previewing images in a batch:

import { useEffect, useState } from "react";

const imageTypeRegex = /image\/(png|jpg|jpeg)/gm;

function App() {
  const [imageFiles, setImageFiles] = useState([]);
  const [images, setImages] = useState([]);

  const changeHandler = (e) => {
    const { files } = e.target;
    const validImageFiles = [];
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      if (file.type.match(imageTypeRegex)) {
        validImageFiles.push(file);
      }
    }
    if (validImageFiles.length) {
      setImageFiles(validImageFiles);
      return;
    }
    alert("Selected images are not of valid type!");
  };

  useEffect(() => {
    const images = [], fileReaders = [];
    let isCancel = false;
    if (imageFiles.length) {
      imageFiles.forEach((file) => {
        const fileReader = new FileReader();
        fileReaders.push(fileReader);
        fileReader.onload = (e) => {
          const { result } = e.target;
          if (result) {
            images.push(result)
          }
          if (images.length === imageFiles.length && !isCancel) {
            setImages(images);
          }
        }
        fileReader.readAsDataURL(file);
      })
    };
    return () => {
      isCancel = true;
      fileReaders.forEach(fileReader => {
        if (fileReader.readyState === 1) {
          fileReader.abort()
        }
      })
    }
  }, [imageFiles]);
  return (
    <div className="App">
      <form>
        <p>
          <label htmlFor="file">Upload images</label>
          <input
            type="file"
            id="file"
            onChange={changeHandler}
            accept="image/png, image/jpg, image/jpeg"
            multiple
          />
        </p>
      </form>
      {
        images.length > 0 ?
          <div>
            {
              images.map((image, idx) => {
                return <p key={idx}> <img src={image} alt="" /> </p>
              })
            }
          </div> : null
      }
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

We keep references to the FileReader instances in an array for canceling any file reading process in the cleanup function when the component re-renders or unmounts to avoid memory leaks.

When using a routing library like React Router, a user can navigate away from the current page, and the component unmounts before completing the file reading process. Therefore, it is necessary to do cleanup as highlighted above.

In the above example, we are asynchronously reading the files in a loop and updating state after. Because of the asynchronous nature of the file reading process, it is impossible to know which file we shall complete reading last. Therefore, we have to check the number of files read in the load event handler before updating state. You can achieve the same with promises.

The code below shows a modification of the useEffect Hook to use promises instead. It is cleaner and easier to think about than using loops like in the previous method:

useEffect(() => {
  const fileReaders = [];
  let isCancel = false;
  if (imageFiles.length) {
    const promises = imageFiles.map(file => {
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReaders.push(fileReader);
        fileReader.onload = (e) => {
          const { result } = e.target;
          if (result) {
            resolve(result);
          }
        }
        fileReader.onabort = () => {
          reject(new Error("File reading aborted"));
        }
        fileReader.onerror = () => {
          reject(new Error("Failed to read file"));
        }
        fileReader.readAsDataURL(file);
      })
    });
    Promise
      .all(promises)
      .then(images => {
        if (!isCancel) {
          setImages(images);
        }
      })
      .catch(reason => {
        console.log(reason);
      });
  };
  return () => {
    isCancel = true;
    fileReaders.forEach(fileReader => {
      if (fileReader.readyState === 1) {
        fileReader.abort()
      }
    })
  }
}, [imageFiles]);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Most web applications that require image upload from a client's storage device also come with features for previewing images. Among other reasons, previewing an image ensures your clients upload image files of the appropriate type, quality, and size.

You can initiate file upload from a client's device with an input element of type file or using the drag and drop interface. After selecting images, you can preview them using the URL API or the FileReader API. Though using the URL API may be straightforward, the FileReader API is not.

As highlighted in the article, you preview images singly or in a batch. Hopefully, this article gave you insights on image previews in React using the FileReader API. Let me know what you think in the comments section below.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.

Top comments (0)