Build a custom image uploader with ReactJS
Tags: react, webdev, beginners, codenewbie
Published: 2024-03-11 06:37:44

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. 🙂

{% youtube nOqsd8LoUYs %} 

## 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](https://github.com/atapas/vite-tailwind-react) 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](https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-12-at-5.25.29-PM.png)

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1709734991249/16c690b0-179d-4997-bd2e-ba7ac1279dec.png)

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](https://nodejs.org/en/download).

```bash
## With NPM
npm install

## With Yarn
yarn install

## With PNPM
pnpm install
```

Now you can run the project locally using

```bash
## With NPM
npm run dev

## With Yarn
yarn dev

## With PNPM
pnpm run dev
```

The application will be available to access on the URL [`http://localhost:5173/`](http://localhost:5173/).

![Initial UI](https://cdn.hashnode.com/res/hashnode/image/upload/v1709786287217/85939da4-6180-4d0d-8794-2bcd05a0fcc5.png)

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](https://github.com/atapas/youtube/tree/main/react/28-react-image-uploader/src/assets) 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,

```javascript
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";
```

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

```javascript
// --- 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;
```

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1709814033256/c7254a80-e8b1-4fe1-9e44-746c51acc26d.png)
    
    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,

```javascript
import ImageUpload from "./components/ImageUpload"

function App() {

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

export default App;
```

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!).

```xml
<!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>
```

Let us go over the browser and see the app.

![The Uploader with a placeholder image and edit icon](https://cdn.hashnode.com/res/hashnode/image/upload/v1709733935322/0b793c70-5380-4dc6-a2ad-649a7900dc41.png)

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.

```javascript
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>
    )
}
```

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1709797210797/6fddc6be-4694-499f-94b8-4be663016d6e.png)

## 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](https://www.youtube.com/watch?v=LNwEpMLLFTw). Please check it out.

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

```xml
<input 
   type="file"
   id="file"
   hidden />
```

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

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

  const fileUploadRef = useRef(null);

  // --- Rest of the code as is below
}
```

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

```xml
 <input 
    type="file"
    id="file"
    ref={fileUploadRef}
    hidden />
```

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.

```xml
<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>
```

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

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

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.](https://cdn.hashnode.com/res/hashnode/image/upload/v1709805492811/68cd016e-1dcd-4460-8d7e-81815ffb1f72.gif)

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

```javascript
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;
```

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.

```xml
<input 
    type="file"
    id="file"
    ref={fileUploadRef}
    onChange={uploadImageDisplay}
    hidden />
```

Now let's define the `uploadImageDisplay` function.

```javascript
const uploadImageDisplay = async () => {
    const uploadedFile = fileUploadRef.current.files[0];
    const cachedURL = URL.createObjectURL(uploadedFile);
    setAvatarURL(cachedURL);
}
```

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1709734148494/72b5f8ba-75e7-4bd1-8efe-5f0a08a95c1e.png)

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

![File Preview From Client Cache](https://cdn.hashnode.com/res/hashnode/image/upload/v1709734185491/4eb63862-4ea1-4916-b5a8-3f11ad7c4235.png)

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](https://api.escuelajs.co/api/v1/files/upload) 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.

```javascript
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);
    }
}
```

Let us understand the function above:

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

![The Loading State](https://cdn.hashnode.com/res/hashnode/image/upload/v1709734245685/1d2dd125-3492-4d60-84d3-7b20558538a3.png)

* 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1709734320273/bf73a968-eaf6-45d0-bbbb-6bccdaf5466b.png)

* 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:

```javascript
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;
```

## 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](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](https://discord.gg/BAQbwKASDJ) where we learn and share things.

---

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

* [**tapaScript on YouTube**](https://www.youtube.com/tapasadhikary?sub_confirmation=1)
    
* [**X(eka Twitter)**](https://twitter.com/tapasadhikary)
    
* [**LinkedIn**](https://www.linkedin.com/in/tapasadhikary/)
    
* [**GitHub**](https://github.com/atapas)