How-to: React Image Preview Upload using FileReader

Simple component for uploading images with preview built with React using FileReader.

We will be working inside the Next.js using Typescript, but you can use whatever setup you want.

Initial Setup

Create template: Next.js app using Create-Next-App

npx create-next-app <your-app-name>

Follow instructions to start the development server.

If you’re doing a manual set up, add the script below in your <package.json>


"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

Go to your index.tsx file where you’ll initially see a Home component that is returning a form.

export default function Home() {
  return (
    <div className={styles.container}>
      <form>form</form>
    </div>
  );
}

To get started, let’s add an of type file to get or upload actual files from our computer.

export default function Home() {
  return (
    <div className={styles.container}>
      <form>
        <input type="file" />
      </form>
    </div>
  );
}

Just with that, we already have something to get it up and running. And if you look at your browser, you should be able to see this image:

And when you click it, you should be able to access the files in your local machine.

But first, let’s put some styling into our form. Let’s display the input into “none’ and add a button that we can style.

import React, { useRef, useState, useEffect } from "react";
import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={styles.container}>
      <form>
        <button> Add Image</button>
        <input type="file" style={{ display: "none" }} />
      </form>
    </div>
  );
}

The styling is coming from a global stylesheet file.


img,
button {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  cursor: pointer;
}

button {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  border: none;
  background: #fef289;
}

At the moment, this button is not connected to our form input. And to do that, we will use a useRef hook.

export default function Home() {
    const fileInputRef = useRef<HTMLInputElement>();

...

Then let’s add an event in our button but put an action to disable the submit action of the button.

<button
          onClick={(event) => {
            event.preventDefault();
          }}
        >

Before we call the fileInputRef that we’ve created, let’s just a ref to our form input.

...
<input type="file" style={{ display: "none" }}
ref={fileInputRef} />
</form>

And in our button:

<button
          onClick={(event) => {
            event.preventDefault();
            fileInputRef.current.click();
          }}
        >
          Add Image
 </button>

Now using our stylish button, we should be able to upload a file as before.

The next question to ask is where are we storing the file that we have clicked? And for that, we will need to create a state for that in our app.

//with a type of File 
const [image, setImage] = useState<File>();

And to put a value in our newly-created state, let’s create an event in our form input. In the callback function , we’ll access the target value – which is the file. We’ll also put a guardrail to accept only an image file, and not other type of file.

//the [0] is the first indexed or one selected file 
//accept - only allows image file to be selected

          accept="image/*"
          onChange={(event) => {
            const file = event.target.files[0];
            if (file && file.type.substr(0, 5) === "image") {
              setImage(file);
            } else {
              setImage(null);
            }
          }}

Now that we are done with that. Let’s create a preview of the image to be displayed once the user selected an image. And for that, we’ll need to use another hook called . In this hook, we’ll look out for changes to the image state.

useEffect(() => {
    
}, [image]);

And in this image, what we want to do is to extract a string that represents the selected image. To do that, we will be using .

const reader = new FileReader();

What the basically does is to ‘readAsDataURL’ the uploaded image. Take note that the ‘readAsDataURL’ is an encoded base64 string.

useEffect(() => {
  if (image) {
    const reader = new FileReader();
    reader.readAsDataURL(image);
  } else {
  }
}, [image]);

But how do we know when the loading of an image has happened? For that, let’s add <reader.onloadend> and put the result of that function in another state.

if (image) {
  const reader = new FileReader();
  reader.onloadend = () => {
    //result of this reader will be put in a new state 
  };

Let’s create a new useState. The data type is string because the is a base64 string.

const [preview, setPreview] = useState<string>();

We can now use the in our useEffect.

useEffect(() => {
  if (image) {
    const reader = new FileReader();
    reader.onloadend = () => {
      setPreview(reader.result as string);
    };
    reader.readAsDataURL(image);
  } else {
    setPreview(null)
  }
}, [image]);

We can check if we are really getting a string that represents the uploaded image.

Let’s temporarily put <preview> inside a <p> in out form. And try to upload an image. You will get a long string that represents the uploaded file.

<form>
  <p> {preview}</p>
  <button

Now, let’s create the image source where we can put the preview. But we will make it so that we will only show either the image or the button.

<form>
  {preview ? (
    <img src={preview} alt={"preview"} style={{ objectFit: "cover" }} />
  ) : (
    <button
      onClick={(event) => {
        event.preventDefault();
        fileInputRef.current.click();
      }}
    >
      Add Image
    </button>
  )}

Let’s test it out.

You should be able to upload and see a preview of that image in your browser. One final thing, let’s add

another event <onClick> to unset the current image and set back the button to allow us to upload a new image.

At this point, we already have the file image in state and we can post it to a server or to an S3 bucket or wherever you want.