MEDIA GUIDES / Front-End Development

Making a Simple React.js File Upload Component

Many applications today include features that allow users to upload files onto their database or server. The HTML input element and the JavaScript Files API allows users to upload files in a website or application and access the file content, respectively. In React, file uploads take a slightly different approach compared to using vanilla JavaScript.

In this article, we’ll walk you through the process of building a simple React. js file upload component, without relying on external libraries. We’ll cover everything from single file uploads using the Fetch API to more advanced topics like handling multiple files and integrating with Cloudinary for cloud-based media management.

Set up a React Project

Before we proceed, you need to set up a React application for the tutorial. We’ll be using Vite for the setup but if you have an existing application, you can still follow along.

Create a new directory and run the following command and follow the prompts to create a new React app with Vite:

 npm create vite@latest -- --template react-ts . 

Create File Upload Component

Let’s start creating our React.js file upload component. We’ll also use functional components with React hooks to manage the application state.

Create a file named Uploader.tsx and add the following code to it:

import React, { useState } from "react";

const Uploader = () => {
  const [file, setFile] = useState<File | null>(null);
  const [uploadStatus, setUploadStatus] = useState({
    isUploading: false,
    success: false,
  });
  const [selected, setSelectedStatus] = useState(false);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = e.target.files ? e.target.files[0] : null;
    if (selectedFile) {
      setFile(selectedFile);
      setSelectedStatus(true);
    }
  };

  const handleUpload = async () => {
    if (!file) return;

    setSelectedStatus(false);
    setUploadStatus({ ...uploadStatus, isUploading: true });

    // Simulate file upload
    setTimeout(() => {
      setUploadStatus({ ...uploadStatus, isUploading: false, success: true });
      setFile(null);
    }, 2000);
  };

  const handleRemove = () => {
    setFile(null);
  };

  return (
    <div className="container">
      {!uploadStatus.success && (
        <div className="input-wrapper">
          <input
            type="file"
            id="hiddenFileInput"
            style={{ display: "none" }}
            onChange={handleFileChange}
          />

          {!file && (
            <button
              className="upload-btn"
              onClick={() =>
                document.getElementById("hiddenFileInput")?.click()
              }
            >
              Select File
            </button>
          )}
        </div>
      )}

      {file && (
        <div className="upload-wrapper">
          <div className="upload-container">
            <div className="file-info">
              {selected && <div className="success-checkmark" />}

              {uploadStatus.isUploading && <div className="loader" />}
              <div className="file-text">
                <span className="file-name">{file.name}</span>
              </div>
            </div>
            <div className="remove-btn" onClick={handleRemove}>
              ×
            </div>
          </div>
          <button className="upload-btn" onClick={handleUpload}>
            Upload
          </button>
        </div>
      )}

      {uploadStatus.success && (
        <div className="success-wrapper">
          <div className="success-checkmark" />
          <p style={{ color: "black" }}>File uploaded successfully</p>
        </div>
      )}
    </div>
  );
};

export default Uploader; 

Here’s what’s going on in the above code.

  • We use useState to track the selected file (file), upload status (uploadStatus with isUploading and success), and whether a file has just been selected (selected)
  • The handleFileChange function is triggered when the user selects a file using the hidden input element and saves the file to state
  • The handleUpload function simulates the process of uploading the selected file using a timeout function. In a real-world scenario, this could mean saving the file to a database or server. In the next sections, we’ll use this function to save the uploaded file to Cloudinary
  • handleRemove removes the currently selected file from state. This is useful in case the user wants to select a different that the current one
  • Next, a hidden file input is triggered when the Select File button is clicked; after a file is selected, it’s stored in state and we display its name
  • When the Upload button is clicked, it simulates a file upload with setTimeout, updates the UI to show a loading indicator, and then displays a success message.

For the styles, open src/App.css and add the following code to it:

body {
  width: 100%;
  height: 100%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background-color: black;
}

.container {
  width: 500px;
  height: auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background-color: rgb(236, 232, 232);
  border-radius: 6px;
  padding: 50px 15px;
}

.input-wrapper {
  display: flex;
  justify-content: center;
  width: 400px;
  border: 1px dashed #211e1e;
  height: 100px;
  align-items: center;
  margin: 0 auto;
}

.upload-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.upload-container {
  border: 1px solid #ccc;
  border-radius: 6px;
  padding: 10px 14px;
  width: 400px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: sans-serif;
  background-color: #fff;
}

.loader-placeholder {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #eee;
}

.success-checkmark {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background-color: #4caf50;
  display: flex;
  align-items: center;
  justify-content: center;
}

.success-checkmark::before {
  content: "✔";
  font-size: 14px;
  color: white;
  font-weight: bold;
}

.success-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.file-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.loader {
  width: 20px;
  height: 20px;
  border: 3px solid #ccc;
  border-top: 3px solid #666;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.file-text {
  display: flex;
  flex-direction: column;
  font-size: 14px;
}

.file-name {
  font-weight: 500;
  color: #333;
}

.remove-btn {
  font-size: 18px;
  color: #888;
  cursor: pointer;
  transition: color 0.2s ease;
}

.remove-btn:hover {
  color: #f00;
}

.upload-btn {
  background-color: #333;
  height: 30px;
  color: white;
  border: none;
  padding: 8px 18px;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  margin-top: 12px;
  transition: background-color 0.2s ease;
}

.upload-btn:hover {
  background-color: #555;
  outline: none;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
} 

Now, we need to import the uploader into a React.js file upload component where it’ll be used. We’ll import it into src/App.tsx, but you can also import the uploader component into any part of your code.

This is what src/App.tsx looks like now:

import Uploader from './Uploader'
import './App.css'

function App() {
  return (
    <>
  <Uploader/>
    </>
  )
}

export default App 

Here’s the final output.

Handling Multiple File Uploads

The previous React.js file upload component implementation only supports uploading a single file at a time. To enable multiple file uploads, we need to make a few adjustments to our code.

First, we need to add the multiple attribute to the <input> element. This allows users to select more than one file at once.

Then, we need to update our handler functions (handleFileChange, handleUpload, and handleRemove) to work with the arrays of files. Finally, we need to modify the JSX to display the list of selected files.

Here’s the updated version of Uploader.tsx , allowing multiple file uploads:

import React, { useState } from "react";

const Uploader = () => {
  const [files, setFiles] = useState<File[]>([]);
  const [uploadStatus, setUploadStatus] = useState({
    isUploading: false,
    success: false,
  });
  const [selected, setSelectedStatus] = useState(false);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFiles = e.target.files ? Array.from(e.target.files) : [];
    if (selectedFiles.length > 0) {
      setFiles(selectedFiles);
      setSelectedStatus(true);
    }
  };

  const handleUpload = async () => {
    if (files.length === 0) return;

    setSelectedStatus(false);
    setUploadStatus({ ...uploadStatus, isUploading: true });

    // Simulate file upload
    setTimeout(() => {
      setUploadStatus({ ...uploadStatus, isUploading: false, success: true });
      setFiles([]);
    }, 2000);
  };

  const handleRemove = (index: number) => {
    setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
  };

  return (
    <div className="container">
          {files.length === 0 && (
            <div className="input-wrapper">
              <input
                type="file"
                multiple
                id="hiddenFileInput"
                style={{ display: "none" }}
                onChange={handleFileChange}
              />
              <button
                className="upload-btn"
                onClick={() =>
                  document.getElementById("hiddenFileInput")?.click()
                }
              >
                Select File
              </button>
            </div>
      )}

      {files.length > 0 && (
        <div className="upload-wrapper">
          {files.map((file, index) => (
            <div key={index} className="upload-container">
              <div className="file-info">
                {selected && <div className="success-checkmark" />}

                {uploadStatus.isUploading && <div className="loader" />}
                <div className="file-text">
                  <span className="file-name">{file.name}</span>
                </div>
              </div>
              <div className="remove-btn" onClick={() => handleRemove(index)}>
                ×
              </div>
            </div>
          ))}
          <button className="upload-btn" onClick={handleUpload}>
            Upload
          </button>
        </div>
      )}

      {uploadStatus.success && (
        <div className="success-wrapper">
          <div className="success-checkmark" />
          <p style={{ color: "black" }}>File uploaded successfully</p>
        </div>
      )}
    </div>
  );
};

export default Uploader; 

Now users can select multiple files at once as shown in the image below:

With the files are rendered as shown below:

How to Use Fetch API for Uploading Files

In a real-world application, you’d typically need to upload the selected file to a server or cloud storage service. This involves sending the file using an HTTP request, with the file wrapped in a FormData object. You can achieve this using the built-in Fetch API or a library like Axios. However, in this guide, we’ll demonstrate how to handle file uploads by sending them to an external service using the Fetch API.

The Fetch API is a modern, promise-based interface for making HTTP requests and fetching resources in JavaScript. FormData is a Web API interface that provides a way to construct a set of key/value pairs representing form fields and their values. It’s particularly useful for sending binary data, such as files, because it can efficiently handle the encoding and formatting required for file uploads in HTTP requests.

Now let’s modify the handleUpload function to use Fetch API to send the data to an API, for example.

const handleUpload = async () => {
    if (!file) return;
    setSelectedStatus(false);

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

    try {
      const response = await fetch("https://your-api.com/upload", {
        method: "POST",
        body: formData,
      });

      if (!response.ok) {
        throw new Error("File upload failed");
      }

      const result = await response.json();
      console.log("Upload successful:", result);

      setUploadStatus({ ...uploadStatus, isUploading: false, success: true });
      setFile(null);
    } catch (error) {
      console.error("Error uploading file:", error);
      setUploadStatus({ ...uploadStatus, isUploading: false, success: false });
    }
  }; 

Making a Simple React.js File Upload Component for Cloudinary

When working with file uploads in React applications, you’ll often need a database or server to send the files to. Cloudinary is a cloud-based media management platform that simplifies the process of uploading, storing, and delivering images and videos. You can use Cloudinary as a database to simplify your backend, reducing the need for complex server-side code.

There are a couple of ways to upload files to Cloudinary in a React application. We can use any of the following:

  • Cloudinary’s pre-built Upload Widget, which can be customized and embedded within your web application with just a few lines of code.
  • Cloudinary’s React SDK using the @cloudinary/url-gen and @cloudinary/react libraries.
  • Making a Fetch API call to Cloudinary’s upload endpoint https://api.cloudinary.com/v1_1/${cloudName}/upload from the client-side and passing: an unsigned upload preset with the upload method options you want to apply for all files, the file(s) to upload, and other unsigned upload parameters to apply to the selected files.

We’ll go with the third option since we want to avoid using external libraries in this tutorial.

An Upload Preset (required for unsigned uploading and optional for signed uploading) is a set of upload options that you define instead of specifying them in each upload call to the Cloudinary upload endpoint.

You can create an Upload Preset programmatically, or use the Upload Presets settings UI in your Cloudinary dashboard. We’ll go with the latter option in this guide. To create your own Upload Preset, follow the steps here. Make note of your Cloudinary cloud_name and upload_preset. You’ll need them soon.

Here’s how we can adapt our basic React.js file upload component to upload files directly to Cloudinary.

First, add the following code before the handleFileChange function:

const cloudName = 'your_cloud_name'; // Replace with your Cloudinary cloud name
  const uploadPreset = 'your_upload_preset'; // Replace with your Cloudinary upload preset
  const cloudinaryUploadUrl = `https://api.cloudinary.com/v1_1/${cloudName}/upload`; 

Then modify handleFileUpload to upload the file to Cloudinary:

const handleUpload = async () => {
    if (!file) return;
    setSelectedStatus(false);

    const formData = new FormData();
    formData.append('file', file);
    formData.append('upload_preset', uploadPreset);

    try {
      const response = await fetch(cloudinaryUploadUrl, {
        method: 'POST',
        body: formData,
      });

      if (!response.ok) {
        throw new Error("File upload failed");
      }

      const result = await response.json();
      const fileCloudinaryURL = result.secure_url;
      console.log("File uploaded successfully:", fileCloudinaryURL);

      setUploadStatus({ ...uploadStatus, isUploading: false, success: true });
      setFile(null);
    } catch (error) {
      console.error("Error uploading file:", error);
      setUploadStatus({ ...uploadStatus, isUploading: false, success: false });
    }
  }; 

And there you have it. Now you can upload files seamlessly to Cloudinary in your React application.

Empower your development team with Cloudinary’s easy-to-use APIs and SDKs. Sign up for free today!

QUICK TIPS
Colby Fayock
Cloudinary Logo Colby Fayock

In my experience, here are tips that can help you better design and scale your React file upload components, especially when integrating with cloud services like Cloudinary:

  1. Use optimistic UI updates with rollback
    Display the file and success UI immediately after initiating upload (optimistic update), and only rollback if the upload fails—this creates a snappier UX, especially with large files and slower connections.
  2. Throttle or debounce large file selections
    When users upload multiple large files, implement a throttled queue or debounce mechanism to avoid browser crashes and ensure the server/cloud API isn’t overwhelmed with concurrent requests.
  3. Chunk uploads for large files using resumable uploads
    Break files into smaller chunks using slice() and send each part sequentially with progress feedback. Cloudinary supports chunked uploads via advanced SDKs or server-side APIs if needed.
  4. Pre-generate Cloudinary upload signatures from your backend
    Even if you’re using unsigned presets in dev, use backend-generated signatures in production for enhanced security—this prevents abuse of your Cloudinary quota via exposed upload endpoints.
  5. Validate MIME types and file size before uploading
    Check file metadata using JavaScript APIs (file.type, file.size) before upload. This avoids unnecessary API calls and lets you display error messages early for a better user experience.
  6. Isolate component state per upload slot
    When uploading multiple files, track individual upload statuses and errors per file using an array of objects instead of shared state. This gives granular feedback and avoids global state issues.
  7. Persist upload state across reloads
    Store selected files and upload progress in localStorage or IndexedDB. If a user accidentally reloads the page, they can resume their upload session without starting over.
  8. Track analytics and errors from uploads
    Log file types, upload duration, failures, and file sizes to a service like Sentry, Segment, or GA. This helps identify patterns (e.g., frequent failures with large .MOV files) and improve your pipeline.
  9. Compress images client-side before upload
    Use libraries like browser-image-compression or the Canvas API to resize and compress images in the browser. This saves bandwidth and speeds up upload without degrading visible quality.
  10. Provide secure download URLs with expiry tokens
    For sensitive content, use time-limited signed Cloudinary URLs or route downloads through your backend. This prevents hotlinking or unauthorized reuse of uploaded files.
Last updated: May 1, 2025