At some point in your career, you will need to implement file uploading in your application. While React simplifies building user interfaces, handling large-scale file uploads can still be a significant challenge.
This guide walks you through the step-by-step implementation of file uploads in a React application. Once we're done, you'll have an app that looks like this:
Here’s a summary of what you’ll learn in this tutorial:
- Setting up a React application
- Creating a customized user interface for uploads
- Setting up a backend to accept and store files
- Implementing an upload progress bar
- Adding basic validations to upload
- Important considerations for security, scalability, and flexibility
Excited? Let’s jump right in! 🚀
You can check out the live demo on CodeSandbox and explore the code on GitHub.
What do you need to continue?
Before we begin, make sure you have all the prerequisites in place. You'll need Node.js - download the latest LTS version from the official website (for this project, we will be using Node 20.13.0). It is also recommended that you have a basic understanding of HTML, CSS, JavaScript/TypeScript, React, and some backend technology like Node.js. With these in place, you're ready to move forward!
Setting up the project
To get started, we will generate a new React application by running the following commands:
npm create vite@latest react-file-upload -- --template react-ts
cd react-file-upload
npm install
This initializes a React project with a fast, minimal setup using Vite and Typescript.
Now, let’s start a development server using npm run dev
command.
❯ npm run dev
> react-file-upload@0.0.0 dev
> vite
VITE v6.0.11 ready in 123 ms
➜ Local: <http://localhost:5173/>
➜ Network: use --host to expose
➜ press h + enter to show help
When we navigate to the url provided by the output of the command, we are greeted with a screen like this.

Before starting to code, I like to remove the boilerplate code so that our project is minimal. This step is completely optional.
After cleaning up, the App.tsx
should look something like this
function App() {
return (
<>
<h1>Vite + React</h1>
<div className="card">
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
);
}
export default App;
We’ll also just keep the basic styling in index.css
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #131313;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
Setting up the upload form
Now that our project is set up, let's build the file upload component!
We’ll create a basic user interface for uploading files
import { File as FileIcon, Trash as TrashIcon, Upload as UploadIcon } from "@phosphor-icons/react";
import { useRef, useState } from "react";
const styles: Record<string, CSSProperties> = {}
function App() {
const inputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [status, setStatus] = useState<"pending" | "uploading" | "success" | "error">("pending");
const handleUpload = () => {
setStatus("uploading");
setTimeout(() => {
setStatus("success");
}, 2000);
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleUpload();
}}
onReset={() => {
setStatus("pending");
setFile(null);
}}
>
<h1 style={styles.heading}>Upload Files</h1>
<div style={styles.uploadBox} onClick={() => inputRef.current?.click()}>
<div style={styles.content}>
<div style={styles.circle}>
<UploadIcon size={32} />
</div>
<h2 style={styles.uploadText}>
Drop your files here,
<br /> or browse
</h2>
</div>
</div>
<input
type="file"
onChange={(e) => {
setStatus("pending");
setFile(e.target.files?.[0] || null);
}}
hidden
ref={inputRef}
/>
{file && (
<>
<div style={styles.fileList}>
<div style={styles.fileElement}>
<div style={styles.fileIcon}>
<FileIcon size={32} />
</div>
<div style={styles.fileDetails}>
<p style={styles.fileName}>{file.name}</p>
<p style={styles.fileSize}>{(file.size / 1024).toFixed(2)} KB</p>
</div>
<div style={styles.filler} />
{status === "pending" && (
<button
style={styles.trash}
onClick={(e) => {
e.preventDefault();
if (inputRef.current) {
inputRef.current.value = "";
}
setFile(null);
}}
>
<TrashIcon size={32} />
</button>
)}
{status === "uploading" && <p style={styles.uploadingText}>Uploading...</p>}
{status === "success" && <p style={styles.successText}>File uploaded successfully!</p>}
{status === "error" && <p style={styles.errorText}>File upload failed!</p>}
</div>
</div>
<div style={styles.uploadButtonContainer}>
{status === "success" ? (
<button style={styles.uploadButton} type="reset">
Reset
</button>
) : (
<button style={styles.uploadButton} type="submit" disabled={status === "uploading"}>
Upload Files
</button>
)}
</div>
</>
)}
</form>
);
}
export default App;

The code above defines a React component that allows users to upload files. The component uses the useRef and useState hooks to manage the state of the component. The useRef hook is used to create a reference to the file input element. The useState hook is used to manage the state of the file, the status of the upload, and the styles of the component.
The component has three main functions: handleUpload, handleDragOver, and handleDrop. The handleUpload function is called when the user clicks the upload button. It sets the status to "uploading" and then sets the status to "success" after 2 seconds
Now, let’s change the appearance of this form to make it look awesome.
We’ll create a file called styles.ts that will store our styles for our application, which we can get from here and then import it in App.tsx
import { File as FileIcon, Trash as TrashIcon, Upload as UploadIcon } from "@phosphor-icons/react";
import { useRef, useState } from "react";
- const styles: Record<string, CSSProperties> = {}
+ import styles from "./styles";
This is what the application looks like after adding the styles

Now, let’s add the drag-and-drop functionality into the elements
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setStatus("pending");
setFile(e.dataTransfer.files[0]);
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
};
const handleUpload = () => {
setStatus("uploading");
const formData = new FormData();
formData.append("file", file!);
const xhr = new XMLHttpRequest();
xhr.open("POST", "<http://localhost:3000/upload>");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
setStatus("success");
} else {
setStatus("error");
}
}
};
xhr.send(formData);
};
In the above code, we are defining functions to handle the following tasks.
- The handleDrop function is called when a file is dropped on the element that the function is attached to. It prevents the default browser behavior of dropping a file, which is to open the file in the browser. It then sets the file state to the first file in the dataTransfer object.
- The handleDragOver function is called when a file is dragged over the element that the function is attached to. It prevents the default browser behavior of dropping a file, which is to open the file in the browser.
- The handleUpload function is called when the user clicks the upload button. It sets the status state to "uploading". It then creates a new FormData object and appends the file state to it. It then creates a new XMLHttpRequest object and sets the open method to "POST" and the url to "http://localhost:3000/upload". It then sets the onreadystatechange property of the xhr object to a function that checks the readyState and status of the request. If the request is successful, the status state is set to "success". Otherwise, the status state is set to "error". Finally, the xhr object is sent with the formData object.
Now let’s bind these functions to the elements we have in DOM
<div
style={styles.uploadBox}
onClick={() => inputRef.current?.click()}
+ onDrop={handleDrop}
+ onDragOver={handleDragOver}
>
<div style={styles.content}>
<div style={styles.circle}>
<UploadIcon size={32} />
</div>
<h2 style={styles.uploadText}>
Drop your files here,
<br /> or browse
</h2>
</div>
</div>
At this point if we try to upload a file, we will get an error saying “File upload failed!”.
This is because there is no backend server to upload to, so let’s set that up in the next step.

Setting up the upload backend
Let’s switch gears to the server-side implementation.
For the scope of this tutorial, we will be using NodeJS and saving the files in the local filesystem.
We will start by creating a new folder in the project called server.
In this folder, we will run the following commands to setup a basic express.js server with multer to allow file uploads
npm init
npm install express multer cors
Now we will create a file called index.js
in the server folder.
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const cors = require("cors");
const app = express();
const upload = multer();
app.use(cors());
const uploadDir = "uploads";
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
app.post("/upload", upload.single("file"), (req, res) => {
const file = req.file;
if (!file) {
return res.status(400).send({ status: "error", message: "No file uploaded" });
}
const filePath = `${uploadDir}/${file.originalname}`;
try {
fs.writeFileSync(filePath, file.buffer);
res.send({ status: "success", message: "File uploaded successfully" });
} catch (error) {
res.status(500).send({ status: "error", message: "Failed to save file" });
}
});
app.listen(3000);
Let's understand what is happening in the code above:
- Importing and initializing modules
The application requires several modules to function, including a web framework, a middleware for handling file uploads, a file system module for file operations, and a module to handle Cross-Origin Resource Sharing (CORS). After importing them, it creates an instance of the web framework and a configured instance of the file upload middleware. - Enabling CORS
The application enables Cross-Origin Resource Sharing (CORS) to allow requests from other domains or ports. This is useful if the client-side and server-side are hosted on different URLs. - Configuring the file uploads directory
A directory name (for example, uploads) is assigned to store the uploaded files. The code checks if this directory exists on the server, and if it doesn’t, it is created. - Handling file upload requests
The code sets up a route that accepts POST requests for file uploads. When a file is sent to this route, the middleware processes it and attaches the file data to the request object. - Writing the uploaded file to disk
In the route handler, the file’s buffer (which contains its content) is written to the specified directory using the file’s original name. Proper error handling is implemented to respond with an error if something goes wrong during file saving. - Starting the server
Finally, the application starts listening for incoming requests on a specific port (in this case, port 3000), making the file upload route accessible.
We can start the server by running the command node index.js
Let’s test it out
With the backend and front end working, we seem to have completed the upload function, so now it’s time to test it out once.
If things go well, you should be able to see a green message saying “File uploaded successfully!” right next to the file's name.

Implementing upload progress bar
Now that we have a working file upload in place let's add a progress bar for better user experience.
In App.tsx
, replace the section where we are displaying the “File uploading” status with the code below
- {status === "uploading" && <p style={styles.uploadingText}>Uploading...</p>}
+ {status === "uploading" && (
+ <div style={styles.uploadingProgress}>
+ <div
+ style={{
+ ...styles.uploadingBar,
+ width: `${progress}%`,
+ }}
+ />
+ <p style={styles.uploadPercent}>{progress}%</p>
+ </div>
+ )}
After that, we’ll add the styles in the styles.ts
file
uploadingProgress: {
backgroundColor: "#3a3a3a",
height: "24px",
marginTop: "12px",
maxWidth: "200px",
width: "100%",
textAlign: "center",
lineHeight: "24px",
position: "relative",
},
uploadingBar: {
backgroundColor: "#0450d5",
height: "24px",
position: "absolute",
width: "100%",
zIndex: 0,
},
uploadPercent: {
color: "white",
margin: 0,
lineHeight: "24px",
position: "absolute",
left: "50%",
transform: "translate(-50%, 0%)",
zIndex: 1,
},
It's time to add the javascript to update the progress bar. This can be achieved by using the progress event fired on the XMLHttpRequest.upload object. The event gives us 3 read-only properties with which we can calculate the percentage of the upload, but we are more interested in the following
total
: The total size of the file being uploadedloaded
: The size of the file that has already been uploaded
So, let’s bind the event to the XHR request to update the progress bar. In the script
tag, add the following javascript.
xhr.open("POST", "<http://localhost:3000/upload>");
xhr.upload.onprogress = (e) => {
const percentLoaded = Math.round((e.loaded / e.total) * 100);
setProgress(percentLoaded);
};
Voila!! You should now have a working progress bar for your file uploads.
Upload Validations
Finally, to make this production-ready, the upload flow should include at least basic file validations to prevent abuse of the API. These validations may be changed to suit business requirements. In addition to server side checks, implementing client side validations in your React application can greatly enhance user experience by catching errors before the file is sent to the server.
Restricting files based on type metadata
On server-side:
const upload = multer({
fileFilter: (req, file, cb) => {
// Allow only JPEG or PNG images
if (file.mimetype === "image/jpeg" || file.mimetype === "image/png") {
cb(null, true); // Accept the file
} else {
cb(new Error("Invalid file type"), false); // Reject the file
}
},
});
This code sets a custom file filter for multer, a middleware used in Node.js for handling file uploads. It checks the MIME type of the uploaded file and allows only JPEG or PNG images to pass. If the file’s type matches "image/jpeg"
or "image/png"
, it calls cb(null, true)
to accept the file. Otherwise, it calls cb(new Error("Invalid file type"), false)
to reject it.
On client-side:
const handleFileChange = (e) => {
const file = e.target.files[0];
// Validate file type before upload
if (file && !["image/jpeg", "image/png"].includes(file.type)) {
alert("Invalid file type. Please select a JPEG or PNG image.");
return;
}
// Proceed with file upload if validations pass
// e.g., setFile(file);
};
This code snippet checks the selected file's type before it is uploaded. If the file type is not JPEG or PNG, an alert notifies the user, preventing unnecessary server requests.
Restricting file size
On server-side:
const upload = multer({
limits: {
fileSize: 1024 * 1024 * 5, // 5MB
},
});
This configuration sets a file size limit for uploads using multer. The limits
property defines various constraints, and here fileSize
is set to 5MB
(5 * 1024 * 1024 bytes). When a user attempts to upload a file, multer will automatically reject any file that exceeds this size limit. If a file is too large, multer will throw an error, which you can handle in your error-handling middleware to inform the user that their file is too big. Implementing file size restrictions helps protect your server from excessive resource usage and ensures that uploads remain manageable and efficient.
On client-side:
const handleFileChange = (e) => {
const file = e.target.files[0];
// Validate file size before upload (limit to 5MB)
if (file && file.size > 1024 * 1024 * 5) {
alert("File size exceeds the 5MB limit.");
return;
}
// Proceed with further processing or file upload if file size is within limit
// e.g., setFile(file);
};
This client side validation checks the file size immediately after selection. If the file size exceeds 5MB, it alerts the user, thus reducing unnecessary load on the server.
Preventing file overwrites
const crypto = require("crypto")
const filePath = `${uploadDir}/${crypto.randomUUID()}-${file.originalname}`;
To prevent overwriting any existing upload, it’s always recommended to set the filePath
with a file name that is not already used in the destination folder One of the simplest ways to achieve this would be to prefix the file name with a random UUID generated by crypto.randomUUID()
By adding both server-side and client-side checks, you create a safer and smoother user experience. The server validations protect your system from malicious or accidental misuse, while the client-side checks give users quick feedback before they even click “Upload.”
Kudos! We have a fully functional file upload widget ready in React. You can see a functional application on CodeSandbox
What's wrong with this approach?
Building your own upload system from the ground up is entirely feasible and ideal for small projects. However, what happens if your application suddenly gains massive popularity? Is your system equipped to manage a surge of users uploading large files simultaneously? Can users maintain optimal upload speeds? These are critical considerations when designing the upload functionality for your application.
While the do-it-yourself (DIY) method is excellent for educational purposes, it often falls short in real-world scenarios.
Common Challenges You May Face
- Limited file storage on servers
Most DIY setups store uploaded files on the server’s disk, which has limited capacity. As your application scales, managing storage becomes a bottleneck. Expanding storage requires upgrading disks or shifting to a distributed storage solution, adding complexity. - High latency with large file uploads
Large files take longer to upload, and users farther from your server experience even more delay. Deploying servers in multiple regions helps, but it increases infrastructure costs and operational overhead. - Security risks in file uploads
File uploads are a common attack vector—malicious files can compromise your server. Implementing robust validation, sandboxing, and security protocols adds to development effort. - Efficient file delivery
Uploading files is only half the challenge—delivering them quickly and in the right format is another. Handling high request volumes, optimizing media, and ensuring seamless playback require additional processing layers.
A Smarter Way to Handle These Challenges
Instead of dealing with these complexities manually, a dedicated media management solution like ImageKit.io simplifies storage, speeds up uploads, enhances security, and ensures optimized file delivery—all without the need for extensive infrastructure management.
Uploading files directly from the browser
Uploading files directly from the browser saves us from handling the file upload on the backend and reduces server load and bandwidth usage. To achieve this, we will need to perform the following steps
- Setup the imagekit-react sdk
- Instead of directly using the upload API from the backend, we'll be using the IKUpload component provided by the React SDK to upload files to the ImageKit Media Library.
- Generate authentication parameters If we want to implement client-side file upload, we will need a
token
,expiry
timestamp, and a validsignature
for that upload.
Setting up the React SDK
We’ll start by using the same frontend we created above.
However, instead of creating and calling the API ourselves, we’ll be using the ImageKit React SDK, which provides a wrapper around the upload API to simplify the process.
Get the Developer Credentials from the ImageKit Dashboard
From the Developer settings in the ImageKit Dashboard, find and save your public key and private key to be used later.

Install and Initialise the React SDK
To install the ImageKit React SDK, run the command
npm install imagekitio-react
After installing, let’s proceed to setting the SDK in our project, in our App.tsx
we will wrap the form
element in IKContext
<IKContext
urlEndpoint="YOUR_IMAGEKIT_ENDPOINT"
publicKey="YOUR_PUBLIC_KEY"
authenticator={async () => {
try {
const response = await fetch("YOUR_BACKEND_ENDPOINT");
const data = await response.json();
return data.token;
} catch (error) {
console.error(error);
}
}}
>
<form ....>
....
</form>
</IKContext>
Now let’s replace our input element on our website with IKUpload component from SDK
<IKUpload
onChange={(e) => {
setProgress(0);
setStatus("pending");
setFile(e.target.files?.[0] || null);
}}
onUploadStart={() => {
setStatus("uploading");
}}
onUploadProgress={(e) => {
const percent = Math.round((e.loaded / e.total) * 100);
setProgress(percent);
}}
onError={() => {
setStatus("error");
}}
onSuccess={(res) => {
console.log(res);
setStatus("success");
}}
hidden
ref={inputRef}
/>
In the first block of code, we initialize the ImageKit SDK by wrapping the form
element with the IKContext
component. This setup is crucial as it provides the necessary configuration for ImageKit to operate seamlessly within your React application. Here's a breakdown of the props used:
urlEndpoint
: This is your unique ImageKit endpoint URL where the files will be uploaded.publicKey
: Your ImageKit public API key, which is essential for authenticating upload requests.authenticator
: An asynchronous function responsible for generating authentication parameters. We'll delve into the specifics of this function in the next section.
Moving on to the second block, we replace the standard file input with the IKUpload
component from the ImageKit SDK.

This component simplifies the upload process and provides several event handlers to manage different stages of the upload lifecycle:
onChange
: Triggered when a user selects a file. It resets the upload progress, sets the status to "pending," and stores the selected file in the component's state.onUploadStart
: Called when the upload process begins, updating the status to "uploading" to inform users that their file is being uploaded.onUploadProgress
: Provides real-time feedback on the upload progress by calculating the percentage of the upload completed and updating the progress state accordingly.onError
: Handles any errors that occur during the upload process by setting the status to "error," allowing you to inform the user and take appropriate actions.onSuccess
: Executed upon a successful upload, logging the response from ImageKit and updating the status to "success" to notify the user that their file has been uploaded successfully.- The
hidden
attribute ensures that the default file input remains invisible, enabling you to design a custom upload button or interface that aligns with your application's aesthetics. Additionally, theref={inputRef}
connects the upload component to a reference, allowing you to programmatically trigger the file selection dialog or access the uploaded file as needed.
Generate authentication parameters
Now that we have done the front end, you might have observed that we have something called authenticator
in the above code. This asynchronous function responsible for generating the necessary authentication tokens by fetching them from your backend server.
For this task we will ImageKit NodeJS SDK in our server to create API that returns authentication parameters. Learn more about how to create the authentication parameters here
Let’s start by installing the NodeJS SDK in our server,
npm install imagekit
Once you are done installing the SDK, in our index.js file, we’ll add the following code
const ImageKit = require("imagekit");
const imagekit = new ImageKit({
publicKey: "YOUR_IMAGEKIT_PUBLIC_KEY",
urlEndpoint: "YOUR_IMAGEKIT_ENDPOINT",
privateKey: "YOUR_IMAGEKIT_PRIVATE_KEY",
});
app.get("/auth", (req, res) => {
res.send(imagekit.getAuthenticationParameters());
});
And we are done; we should have a completely functional application that uploads your files to ImageKit.
Upload Validations using ImageKit Upload
To enforce limits related to file size and file type, we can add checks that the ImageKit backend will evaluate while uploading the files.
You can read more about these checks here
1. Restrict uploads for a specific file type
In the IKUpload component that we used to upload the files, we can add the check, for instance, to allow only images to be uploaded
<IKUpload
...
checks={`"file.mime": "image/"`}
/>
2. Restrict uploads depending on file size
In the IKUpload component that we used to upload the files, we can add the check to check if the being uploaded is less than the specified size. The check below checks if the file is larger than 1024 bytes by smaller than 5MB
<IKUpload
...
checks={`"file.size" > 1024 AND "file.size" < "5mb"`}
/>
What more does ImageKit have to offer?
After completing the upload process, we need a scalable solution to efficiently optimize and deliver these assets with various transformations. ImageKit.io provides a robust platform that delivers a complete solution for comprehensive media management, optimization, and distribution.
Image and Video Optimization
ImageKit.io includes a range of features such as automatic selection of the optimal format, quality enhancement, and metadata manipulation right out of the box to minimize the final size of images. Additionally, ImageKit.io offers extensive video optimization capabilities to ensure your media is efficiently processed.
Image and Video Transformation
Transforming images and videos is seamless with ImageKit.io by simply adjusting URL parameters. You can perform basic tasks like resizing images or more advanced operations such as adding watermarks, smart cropping, or object-aware cropping. For videos, ImageKit.io enables you to generate thumbnails, convert GIFs to MP4s, and stream videos using adaptive bitrate streaming (ABS) for the best viewing experience.
Collaboration & Sharing
ImageKit’s Digital Asset Management (DAM) solution facilitates the organization, sharing, and management of user access with customizable permission levels for individual users and user groups, ensuring secure and efficient collaboration.
Conclusion
In this tutorial, we've covered:
- Building a file upload application in React from scratch.
- Implementing file type and size validations for secure uploads.
- Transitioning to ImageKit.io for direct uploads from the browser, making our application more efficient and scalable.
- Utilizing ImageKit's free upload API, ImageKit React SDK, and ImageKit NodeJS SDK for a streamlined experience with minimal code.
You can check out the live demo on CodeSandbox and explore the code on Github.