Build a custom image uploader with ReactJS
Tapas Adhikary
Posted on March 11, 2024
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.
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.
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
Now you can run the project locally using
## 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/
.
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";
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;
Let's understand the above code snippet:
-
We have created a functional react component(called,
ImageUpload
). The component has a state calledavatarURL
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.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 theApp.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;
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>
Let us go over the browser and see the app.
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>
)
}
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.
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 />
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
}
Add this fileUploadRef
as the value of the ref
attribute to the file input.
<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.
<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.
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.
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;
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 />
Now let's define the uploadImageDisplay
function.
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. Thefiles
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 thefiles
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,
You will see the image got uploaded and it previewed immediately.
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);
}
}
Let us understand the function above:
- Initially, we set the
avatarURL
state value with the loading image.
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 thedata?.location
. We set that as the updated state value. Hence, the loading image will be replaced with the uploaded image.
- 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;
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.
Posted on March 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.