DEV Community

Cover image for Build a custom image uploader with ReactJS
Tapas Adhikary
Tapas Adhikary

Posted on • Originally published at blog.greenroots.info

Build a custom image uploader with ReactJS

Uploading and previewing images are fundamental requirements in any modern user-centric application. Be it Facebook, Instagram, WhatsApp, Twitter, LinkedIn, whatever you name, you have it there.

In this article, we will learn how to handle image upload and preview in a ReactJS application. Just a callout, we will not use any external(3rd-party) library for this. Rather, we will build it from scratch to understand what happens under the hood. With me? Let's get started with the setup.

If you want to learn from the video content, this article is available as a video tutorial. Please take a look. 🙂

Setup a ReactJS app with Vite and TailwindCSS

We will use TailwindCSS with React and by tradition, let us create a React project scaffolding with Vite. The good news is, that you do not have to do all the installation and configurations from scratch. You can use this template repository to create your project repository with Vite and TailwindCSS configured with React.

Click on the Use this template button at the top-right corner of the template repository. It will take you to the next page to provide the new repository details.

Template Repository

On the next page, provide the new repository name, and a suitable description, and create the repository. Let us call the new repository as image-uploader in this article.

Create Repository with details

Now, clone the new repository to get the project folder locally. Change the directory to the project folder and install the required dependencies using npm/yarn/pnpm.

Make sure you have Node.js version 18 or above installed. You can try the command node -v to find out the version installed. If you want to install or upgrade to the required version of Node.js, please do it from here.

## With NPM
npm install

## With Yarn
yarn install

## With PNPM
pnpm install
Enter fullscreen mode Exit fullscreen mode

Now you can run the project locally using

## With NPM
npm run dev

## With Yarn
yarn dev

## With PNPM
pnpm run dev
Enter fullscreen mode Exit fullscreen mode

The application will be available to access on the URL http://localhost:5173/.

Initial UI

Great, we have got the initial setup done. You can import the project into your favourite code editor. Let us now create the image uploader component.

Complete the uploader structure

Create a components directory under the src directory. Now, create a file called ImageUpload.jsx under the src/components. This file will contain the React component to upload and preview images.

Before we start writing the code for the image uploader, we need a few things.

  • A default image to tell our users about the image uploader.

  • An edit icon to invoke the file browser to select an image.

  • A loading gif file we will use as a placeholder when the file upload is in progress.

Please copy the image files from here to your project's src/assets directory. Now, go back to the ImageUpload.jsx file and start coding the component.

First, import the images we just copied. We will also need the useState and useRef hooks from React. So, copy and paste the following import statements at the top of the component file,

import { useState, useRef } from 'react';
import DefaultImage from "../assets/upload-photo-here.png";
import EditIcon from "../assets/edit.svg";
import UploadingAnimation from "../assets/uploading.gif";
Enter fullscreen mode Exit fullscreen mode

Now, let's create the component function right below the import statements.

// --- Import statements as belore

const ImageUpload = () => {
    const [avatarURL, setAvatarURL] = useState(DefaultImage);
        return (
            <div className="relative h-96 w-96 m-8">
              <img 
                src={avatarURL}
                alt ="Avatar"
                className="h-96 w-96 rounded-full" />

              <form id="form" encType='multipart/form-data'>
                <button
                  type='submit'
                  className='flex-center absolute bottom-12 right-14 h-9 w-9 rounded-full'>
                  <img
                    src={EditIcon}
                    alt="Edit"
                    className='object-cover' />
                </button>
              </form>  
            </div>
         )
}

export default ImageUpload;
Enter fullscreen mode Exit fullscreen mode

Let's understand the above code snippet:

  • We have created a functional react component(called, ImageUpload). The component has a state called avatarURL which is set with an initial value, the default image. We did this to ensure that the page is not blunt and blank, and we show an upload message to our users with that default image.

    The Default Image

    We will change the value of the state later programmatically when the user starts uploading an image and when the image upload completes.

  • Next, we have a fairly simple JSX that shows this default image using the avatarURL state value. We also have a form with a button and an edit icon underneath. We are introducing this form early so that we can handle the file input in a while.

  • Finally, we exported the ImageUpload component so that we can import it into the App.jsx file for rendering.

Please open the App.jsx file and replace its content with the following code snippet,

import ImageUpload from "./components/ImageUpload"

function App() {

  return (
    <div className="flex justify-center">
      <ImageUpload />
    </div>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Here we have imported the ImageUpload component and used it in the JSX of the App component. We have surrounded it with a DIV to align it better on the page.

Next, optionally you can change a few things in the index.html file. We have changed the app's title. You can make the change in the <title> tag under the <head>...</head>. Also, we have provided a few classes to the <body> to get a darker background(most folks love dark mode these days!).

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Image Uploader</title>
  </head>
  <body class="bg-black bg-opacity-90">
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Let us go over the browser and see the app.

The Uploader with a placeholder image and edit icon

You can see a yellow rounded image with the Upload text and a hand icon pointing towards an edit icon indicating that you can click on the edit icon to start the uploading process. Cool, huh?

Let us now handle the input file type to select an image for uploading.

Handling the file chooser

Now we need to add a file chooser to our component. We will be using HTML's input element with the type file. Please add the input type file inside the <form> element in the JSX as shown below.

const ImageUpload = () => {
    // --- All code like before

    return (
        {/* --- All code like before */}

        <form id="form" encType='multipart/form-data'>
            {/* --- The Button code like before */}

            <input 
              type="file"
              id="file" />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

If we go to the UI now, we will see a file chooser with an option to Choose file. You can click on it to select files. It works but it doesn't look great. Also, we do not want our users to click on that button to choose files, rather, they should perform the same operation by clicking on the edit icon we added before.

input type file in its raw format

Customizing the uploader with reference(useRef)

let's fix that. We have to do two things:

  • Hide the input file type.

  • Refer to the input file type from the edit icon such that, when we click on the edit icon, it simulates the click on the Choose file button of the file input type(yes, even when it is hidden!).

React provides us with a standard hook called useRef which allows one to refer to a DOM element and get access to its properties. It means we can create a reference to the file input type using the useRef hook and use that reference to simulate a click action on the edit icon click. Does it sound promising yet confusing? Don't worry, let's see the code and understand.

If you are looking for an in-depth understanding of the useRef hook, I have a foundational video here. Please check it out.

First, hide the file input by adding a hidden property to it.

<input 
   type="file"
   id="file"
   hidden />
Enter fullscreen mode Exit fullscreen mode

Now, create a reference using the useRef hook at the top of your component.

const ImageUpload = () => {
  const [avatarURL, setAvatarURL] = useState(DefaultImage);

  const fileUploadRef = useRef(null);

  // --- Rest of the code as is below
}
Enter fullscreen mode Exit fullscreen mode

Add this fileUploadRef as the value of the ref attribute to the file input.

 <input 
    type="file"
    id="file"
    ref={fileUploadRef}
    hidden />
Enter fullscreen mode Exit fullscreen mode

So. from now on, the fileUploadRef.current will refer to the file input element. We will use it to simulate a click event.

Add an onClick property to the <button> inside the form, and add an event handler function called handleImageUpload as a value of it.

<form id="form" encType='multipart/form-data'>
    <button
        type='submit'
        onClick={handleImageUpload}
        className='flex-center absolute bottom-12 right-14 h-9 w-9 rounded-full'>
            <img
                src={EditIcon}
                alt="Edit"
                className='object-cover' />
    </button>
</form>
Enter fullscreen mode Exit fullscreen mode

The last thing that remains is to defile the handleImageUpload function.

 const handleImageUpload = (event) => {
    event.preventDefault();
    fileUploadRef.current.click();
  }
Enter fullscreen mode Exit fullscreen mode

As you see in the code above, we now invoke the click() function using the fileUploadRef.current. It means, the click on the file input will occur and we will see the file chooser opens up when someone clicks on the edit icon.

Clicking on Edit icon opens up the file chooser.

At this stage, the code of the ImageUpload component will be like this:

import { useState, useRef } from "react";
import DefaultImage from "../assets/upload-photo-here.png";
import EditIcon from "../assets/edit.svg";
import UploadingAnimation from "../assets/uploading.gif";

const ImageUpload = () => {
  const [avatarURL, setAvatarURL] = useState(DefaultImage);
  const fileUploadRef = useRef();

  const handleImageUpload = (event) => {
    event.preventDefault();
    fileUploadRef.current.click();
  };

  return (
    <div className="relative h-96 w-96 m-8">
      <img 
        src={avatarURL} 
        alt="Avatar" 
        className="h-96 w-96 rounded-full" />

      <form id="form" encType="multipart/form-data">
        <button
          type="submit"
          onClick={handleImageUpload}
          className="flex-center absolute bottom-12 right-14 h-9 w-9 rounded-full"
        >
          <img 
            src={EditIcon} 
            alt="Edit" 
            className="object-cover" />
        </button>
        <input 
            type="file" 
            id="file" 
            ref={fileUploadRef} 
            hidden />
      </form>
    </div>
  );
};

export default ImageUpload;
Enter fullscreen mode Exit fullscreen mode

All these are great, but, have you noticed that nothing really happens when you select an image and click on the Open button from the file chooser? Let's handle that part now.

Handling the file change event

To capture the selected file and its information, we need to add a change handler on the file input. Please add an onChange property to the file input along with a change handler function uploadImageDisplay as a value.

<input 
    type="file"
    id="file"
    ref={fileUploadRef}
    onChange={uploadImageDisplay}
    hidden />
Enter fullscreen mode Exit fullscreen mode

Now let's define the uploadImageDisplay function.

const uploadImageDisplay = async () => {
    const uploadedFile = fileUploadRef.current.files[0];
    const cachedURL = URL.createObjectURL(uploadedFile);
    setAvatarURL(cachedURL);
}
Enter fullscreen mode Exit fullscreen mode

A few things happening here:

  • We read the uploaded file information from the files[0] property. The files array contains information about all the files selected using the file chooser. As we are supporting(or rather concerned) about only one image file at a time, we can assume the first element of the files array will get us that information.

  • Next, we use the URL.createObjectURL to get the cached path of the uploaded file.

  • Finally, we set that file path as a value to the state variable we have been maintaining to show the default and uploaded image.

Previewing the uploaded image from the browser cache

Now, if you select an image from the file chooser and click on the Open button at the bottom,

File Browser

You will see the image got uploaded and it previewed immediately.

File Preview From Client Cache

This is great! But, it would be even fantastic if we could upload this image to a server and get a link as a response to preview it.

Uploading the image to the server

I have found this public API that supports file upload on a server. So now, we will make a POST call to this API with the selected file as a payload. Let us change the uploadImageDisplay function to make it happen.

const uploadImageDisplay = async () => {
    try {
      setAvatarURL(UploadingAnimation);
      const uploadedFile = fileUploadRef.current.files[0];
      // const cachedURL = URL.createObjectURL(uploadedFile);
      // setAvatarURL(cachedURL);

      const formData = new FormData();
      formData.append("file", uploadedFile);

      const response = await fetch("https://api.escuelajs.co/api/v1/files/upload", {
        method: "post",
        body: formData
      });

      if (response.status === 201) {
        const data = await response.json();
        setAvatarURL(data?.location);
      }

    } catch(error) {
      console.error(error);
      setAvatarURL(DefaultImage);
    }
}
Enter fullscreen mode Exit fullscreen mode

Let us understand the function above:

  • Initially, we set the avatarURL state value with the loading image.

The Loading State

  • We have commented out the code to show the image from the browser cache.

  • We created a form data with the key file as the API expects it this way.

  • Then we used the fetch Web API to make a POST call with the form data as the payload.

  • On receiving the response, we check if the status code is 201, which means the file has been uploaded successfully. We read the response data and get the uploaded image URL from the data?.location. We set that as the updated state value. Hence, the loading image will be replaced with the uploaded image.

Image Preview From the Response

  • If there is an error, we set the state value to the default image.

Congratulations!!! You have done it.

Here is the complete code of the ImageUpload component:

import React, {useState, useRef} from 'react'
import DefaultImage from "../assets/upload-photo-here.png";
import EditIcon from "../assets/edit.svg";
import UploadingAnimation from "../assets/uploading.gif";

const ImageUpload = () => {
  const [avatarURL, setAvatarURL] = useState(DefaultImage);

  const fileUploadRef = useRef();

  const handleImageUpload = (event) => {
    event.preventDefault();
    fileUploadRef.current.click();
  }

  const uploadImageDisplay = async () => {
    try {
      setAvatarURL(UploadingAnimation);
      const uploadedFile = fileUploadRef.current.files[0];
      // const cachedURL = URL.createObjectURL(uploadedFile);
      // setAvatarURL(cachedURL);
      const formData = new FormData();
      formData.append("file", uploadedFile);

      const response = await fetch("https://api.escuelajs.co/api/v1/files/upload", {
        method: "post",
        body: formData
      });

      if (response.status === 201) {
        const data = await response.json();
        setAvatarURL(data?.location);
      }
    } catch(error) {
      console.error(error);
      setAvatarURL(DefaultImage);
    }
  }

  return (
    <div className="relative h-96 w-96 m-8">
      <img 
        src={avatarURL}
        alt ="Avatar"
        className="h-96 w-96 rounded-full" />

      <form id="form" encType='multipart/form-data'>
        <button
          type='submit'
          onClick={handleImageUpload}
          className='flex-center absolute bottom-12 right-14 h-9 w-9 rounded-full'>
          <img
            src={EditIcon}
            alt="Edit"
            className='object-cover' />
        </button>
        <input 
          type="file"
          id="file"
          ref={fileUploadRef}
          onChange={uploadImageDisplay}
          hidden />
      </form>  
    </div>
  )
}

export default ImageUpload;
Enter fullscreen mode Exit fullscreen mode

A couple of tasks for you!

I hope you enjoyed learning from this article and it helped you to understand the concept. All the source code used in this article can be found on my GitHub.

https://github.com/atapas/youtube/tree/main/react/28-react-image-uploader

Before we end, I would like to give you a couple of tasks to implement on top of whatever we have done so far:

  • Right now, the file chooser accepts all sorts of file types. Can you restrict it to accept only the image type files, i.e., .png, .jpg, .svg, etc?

  • Right now, the file chooser appears when we click the edit icon. Can you make the code changes to make it appear when you click on anywhere on the image?

If you happen to do these, please let me know. My socials are mentioned below. You can also join tapaScript discord where we learn and share things.


Let's connect. I share knowledge on web development, content creation, Open Source, and careers on these platforms.

Top comments (0)