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
withisUploading
andsuccess
), 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 withsetTimeout
, 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!