How to Upload File in React JS?

How to Upload File in React JS?

Table of Contents

Quick answer

Upload files in React in 3 steps:

  1. Create file input with <input type="file" onChange={handleFileChange} />
  2. Store file in state: const [file, setFile] = useState(null)
  3. Send to server: const formData = new FormData(); formData.append('file', file); fetch('/upload', { method: 'POST', body: formData })

That’s it. You now have a working file upload.

What is file upload in React?

File upload is the ability to let users select files from their computer and send them to your server. This is fundamental to any app that needs to handle user-generated content: profile pictures, documents, resume uploads, invoice uploads, media files, etc.

In React, file upload involves:

  1. Input handling — Getting the file from the <input type="file"> element
  2. State management — Storing the file in React state (using useState)
  3. Validation — Checking file size, type, and other requirements
  4. Upload — Sending the file to a backend server
  5. Feedback — Showing progress, success, or error messages

Unlike traditional form submissions, React file uploads happen without full page reloads, keeping your single-page app experience smooth and responsive.

Prerequisites

  • A React app running with Vite (from the React + VS Code setup guide)
  • Understanding of React hooks (useState, useRef)
  • A backend endpoint to receive files (Node/Express, PHP, Python, etc.)
  • ~20 minutes

Method 1: Basic file upload (single file)

The simplest way to upload a file

jsx

import { useState } from 'react'

function FileUpload() {
  const [file, setFile] = useState(null)
  const [message, setMessage] = useState('')

  const handleFileChange = (e) => {
    setFile(e.target.files[0]) // Get first file selected
  }

  const handleUpload = async () => {
    if (!file) {
      setMessage('Please select a file')
      return
    }

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

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      })

      if (response.ok) {
        setMessage('File uploaded successfully!')
        setFile(null)
      } else {
        setMessage('Upload failed. Try again.')
      }
    } catch (error) {
      setMessage('Error: ' + error.message)
    }
  }

  return (
    <div>
      <input 
        type="file" 
        onChange={handleFileChange}
        accept=".jpg,.png,.pdf"
      />
      {file && <p>Selected: {file.name}</p>}
      <button onClick={handleUpload}>Upload File</button>
      {message && <p>{message}</p>}
    </div>
  )
}

export default FileUpload

What’s happening:

  • <input type="file"> — Lets users pick a file
  • onChange={handleFileChange} — Captures the selected file
  • FormData() — Converts file to form data (required for file uploads)
  • fetch('/api/upload', { method: 'POST', body: formData }) — Sends to backend

Key point: Always use FormData for file uploads. Regular JSON won’t work.

Method 2: Drag and drop file upload

More user-friendly: let users drag files directly

jsx

import { useState } from 'react'

function DragDropUpload() {
  const [file, setFile] = useState(null)
  const [isDragActive, setIsDragActive] = useState(false)
  const [uploadStatus, setUploadStatus] = useState('')

  const handleDrag = (e) => {
    e.preventDefault()
    e.stopPropagation()
    setIsDragActive(e.type === 'dragenter' || e.type === 'dragover')
  }

  const handleDrop = (e) => {
    e.preventDefault()
    e.stopPropagation()
    setIsDragActive(false)

    const droppedFiles = e.dataTransfer.files
    if (droppedFiles.length > 0) {
      setFile(droppedFiles[0])
    }
  }

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

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

    setUploadStatus('Uploading...')

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      })

      if (response.ok) {
        setUploadStatus('✓ Upload complete!')
        setFile(null)
      } else {
        setUploadStatus('✗ Upload failed')
      }
    } catch (error) {
      setUploadStatus('✗ Error: ' + error.message)
    }
  }

  return (
    <div
      onDragEnter={handleDrag}
      onDragLeave={handleDrag}
      onDragOver={handleDrag}
      onDrop={handleDrop}
      style={{
        border: isDragActive ? '2px dashed blue' : '2px dashed gray',
        padding: '2rem',
        textAlign: 'center',
        cursor: 'pointer'
      }}
    >
      <p>Drag and drop your file here, or click to select</p>
      <input 
        type="file" 
        onChange={(e) => setFile(e.target.files[0])}
        style={{ display: 'none' }}
      />
      {file && <p>Selected: {file.name}</p>}
      <button onClick={handleUpload} disabled={!file}>
        Upload
      </button>
      {uploadStatus && <p>{uploadStatus}</p>}
    </div>
  )
}

export default DragDropUpload

New concepts:

  • onDragEnter, onDragOver, onDragLeave — Track when file hovers over drop zone
  • e.dataTransfer.files — Get dropped files
  • Visual feedback (blue border when dragging)

Method 3: Upload with progress bar

Show real-time upload progress to users

jsx

import { useState } from 'react'

function FileUploadWithProgress() {
  const [file, setFile] = useState(null)
  const [progress, setProgress] = useState(0)
  const [uploading, setUploading] = useState(false)

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

    setUploading(true)
    const formData = new FormData()
    formData.append('file', file)

    try {
      const xhr = new XMLHttpRequest()

      // Track upload progress
      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
          const percentComplete = (e.loaded / e.total) * 100
          setProgress(percentComplete)
        }
      })

      xhr.addEventListener('load', () => {
        setProgress(100)
        setUploading(false)
        setFile(null)
        alert('Upload complete!')
      })

      xhr.addEventListener('error', () => {
        setUploading(false)
        alert('Upload failed')
      })

      xhr.open('POST', '/api/upload')
      xhr.send(formData)
    } catch (error) {
      setUploading(false)
      alert('Error: ' + error.message)
    }
  }

  return (
    <div>
      <input 
        type="file"
        onChange={(e) => {
          setFile(e.target.files[0])
          setProgress(0)
        }}
      />
      {file && <p>File: {file.name}</p>}
      
      {uploading && (
        <div style={{ marginTop: '1rem' }}>
          <div style={{
            width: '100%',
            height: '20px',
            backgroundColor: '#eee',
            borderRadius: '5px',
            overflow: 'hidden'
          }}>
            <div style={{
              height: '100%',
              width: `${progress}%`,
              backgroundColor: '#4CAF50',
              transition: 'width 0.3s'
            }}></div>
          </div>
          <p>{Math.round(progress)}% uploaded</p>
        </div>
      )}

      <button onClick={handleUpload} disabled={!file || uploading}>
        {uploading ? 'Uploading...' : 'Upload'}
      </button>
    </div>
  )
}

export default FileUploadWithProgress

Key: xhr.upload.addEventListener('progress', ...) tracks real-time upload progress.

Method 4: Validation before upload

Validate file type and size on the client

jsx

import { useState } from 'react'

function ValidatedFileUpload() {
  const [file, setFile] = useState(null)
  const [error, setError] = useState('')

  const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
  const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']

  const handleFileChange = (e) => {
    const selectedFile = e.target.files[0]
    setError('')
    
    // Check if file exists
    if (!selectedFile) {
      setFile(null)
      return
    }

    // Check file size
    if (selectedFile.size > MAX_FILE_SIZE) {
      setError(`File too large. Max size: 5MB. Your file: ${(selectedFile.size / 1024 / 1024).toFixed(2)}MB`)
      setFile(null)
      return
    }

    // Check file type
    if (!ALLOWED_TYPES.includes(selectedFile.type)) {
      setError(`Invalid file type. Allowed: JPEG, PNG, PDF. Your file: ${selectedFile.type}`)
      setFile(null)
      return
    }

    setFile(selectedFile)
  }

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

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

    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      })

      if (response.ok) {
        alert('File uploaded successfully!')
        setFile(null)
      }
    } catch (error) {
      setError('Upload error: ' + error.message)
    }
  }

  return (
    <div>
      <input 
        type="file"
        onChange={handleFileChange}
        accept=".jpg,.jpeg,.png,.pdf"
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {file && <p style={{ color: 'green' }}>✓ {file.name} ({(file.size / 1024).toFixed(2)}KB)</p>}
      <button onClick={handleUpload} disabled={!file}>
        Upload
      </button>
    </div>
  )
}

export default ValidatedFileUpload

Validation checks:

  • File size ≤ 5MB
  • File type is JPEG, PNG, or PDF
  • User-friendly error messages

Multiple file uploads

Let users upload multiple files at once

jsx

import { useState } from 'react'

function MultipleFileUpload() {
  const [files, setFiles] = useState([])
  const [uploadStatus, setUploadStatus] = useState('')

  const handleFileChange = (e) => {
    // Convert FileList to Array to keep all files
    setFiles(Array.from(e.target.files))
  }

  const handleUpload = async () => {
    if (files.length === 0) {
      setUploadStatus('Please select at least one file')
      return
    }

    const formData = new FormData()
    
    // Add all files to form data
    files.forEach((file, index) => {
      formData.append(`file-${index}`, file)
    })

    setUploadStatus('Uploading ' + files.length + ' file(s)...')

    try {
      const response = await fetch('/api/upload-multiple', {
        method: 'POST',
        body: formData
      })

      if (response.ok) {
        setUploadStatus('✓ All ' + files.length + ' files uploaded!')
        setFiles([])
      }
    } catch (error) {
      setUploadStatus('✗ Error: ' + error.message)
    }
  }

  return (
    <div>
      <input 
        type="file"
        multiple
        onChange={handleFileChange}
      />
      {files.length > 0 && (
        <div>
          <p>Selected {files.length} file(s):</p>
          <ul>
            {files.map((file, index) => (
              <li key={index}>{file.name} ({(file.size / 1024).toFixed(2)}KB)</li>
            ))}
          </ul>
        </div>
      )}
      <button onClick={handleUpload} disabled={files.length === 0}>
        Upload All
      </button>
      {uploadStatus && <p>{uploadStatus}</p>}
    </div>
  )
}

export default MultipleFileUpload

Key: multiple attribute lets users select multiple files. Loop through files array to add each to FormData.

Upload methods comparison

MethodProsConsBest for
Basic <input>Simple, standard HTMLNo visual feedbackSimple forms, single file
Drag & dropUser-friendly, modern UXRequires event handlingPhoto uploaders, file managers
Progress barShows real-time feedbackRequires XMLHttpRequestLarge files, slow connections
React Dropzone libFeature-rich, pre-builtExtra dependencyComplex upload scenarios
React Hook FormIntegrates with formsLearning curveMulti-step forms

Upload libraries comparison

LibrarySizeFeaturesBest for
None (vanilla)0KBBasic upload onlySimple projects
react-dropzone8KBDrag-drop, validationModern UX requirements
React Dropzone8KBDrag-drop, accessibilityAccessible apps
React Fine Uploader20KBProgress, chunked uploadsLarge files
Uppy25KBDashboard UI, pluginsAdvanced uploads

Recommendation: For most projects, vanilla React (FormData + fetch) is enough. Use libraries only if you need drag-drop or advanced features.

Server-side file handling (Node/Express example)

Your backend needs to receive the file

javascript

// server.js (Express backend)
const express = require('express')
const multer = require('multer') // npm install multer
const app = express()

// Configure file storage
const storage = multer.diskStorage({
  destination: 'uploads/',
  filename: (req, file, cb) => {
    // Sanitize filename to prevent security issues
    const sanitized = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')
    cb(null, Date.now() + '-' + sanitized)
  }
})

const upload = multer({ 
  storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'application/pdf']
    if (allowed.includes(file.mimetype)) {
      cb(null, true)
    } else {
      cb(new Error('Invalid file type'))
    }
  }
})

// Endpoint to receive file
app.post('/api/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' })
  }

  res.json({
    success: true,
    filename: req.file.filename,
    size: req.file.size,
    message: 'File uploaded successfully'
  })
})

app.listen(3000, () => console.log('Server on port 3000'))

Key points:

  • Use multer library to handle file uploads
  • Validate file type and size on server (never trust client)
  • Sanitize filenames to prevent security attacks
  • Store files in a dedicated uploads/ directory

Security best practices

Critical: Always validate on the server

jsx

// ✅ GOOD: Server validates even if client validation fails
// Client validation is for UX, not security

// ❌ BAD: Relying only on client validation
// Attackers can bypass client checks

// ✅ GOOD: Sanitize filenames
const safe = filename.replace(/[^a-zA-Z0-9.-]/g, '_')

// ❌ BAD: Storing user-provided names directly
fs.writeFile(userFilename, data) // Dangerous!

// ✅ GOOD: Check MIME type on server
if (!['image/jpeg', 'image/png'].includes(file.mimetype)) reject()

// ❌ BAD: Trusting file extension
if (file.originalname.endsWith('.jpg')) accept() // Can be faked

// ✅ GOOD: Separate uploads from web root
// Store in /var/uploads, not /var/www/uploads

// ❌ BAD: Making uploads directly executable
// Always treat uploaded files as untrusted

FAQ

Q: Can I upload files without a backend?

A: Not really. You can use services like Firebase Storage or AWS S3 with client-side code, but you still need a backend system. Pure client-side upload requires a server somewhere.

Q: What’s FormData and why do I need it?

A: FormData is a browser API that converts files + data into multipart/form-data format, which is required for file uploads. Regular JSON can’t handle binary files.

Q: How do I preview images before upload?

A: Use FileReader.readAsDataURL():

jsx

const reader = new FileReader()
reader.onload = (e) => setPreview(e.target.result)
reader.readAsDataURL(file)

Q: Can I limit file uploads by extension?

A: Use <input accept=".jpg,.png,.pdf">, but always validate on the server too. Extensions can be faked.

Q: How do I handle very large files (100MB+)?

A: Use chunked uploads (split file into 5-10MB chunks, upload separately, reassemble on server). Libraries like Uppy or Fine Uploader handle this automatically.

Q: Is it safe to validate on the client side only?

A: No. Always validate on the server. Client validation is only for user experience; attackers bypass it easily.

Q: Can I upload to a different domain?

A: Yes, but CORS must allow it. Your server needs Access-Control-Allow-Origin headers. React can’t change browser CORS rules, only servers can.

Q: How do I show upload progress for multiple files?

A: Track progress for each file separately or show overall progress:

jsx

const totalSize = files.reduce((sum, f) => sum + f.size, 0)
const uploadedSize = files.reduce((sum, f) => sum + f.uploaded, 0)
const percentComplete = (uploadedSize / totalSize) * 100

Q: What if the user closes the browser during upload?

A: The request cancels. You can use AbortController to handle cleanup:

jsx

const controller = new AbortController()
fetch(url, { signal: controller.signal })
// Close browser → request aborts

Q: Can I resume interrupted uploads?

A: Yes, using chunked uploads + resumable protocols (like tus.io). Requires backend support.

Q: How do I delete uploaded files?

A: Send a DELETE request with the filename:

jsx

await fetch(`/api/delete/${filename}`, { method: 'DELETE' })

Troubleshooting

“413 Payload Too Large”

Your server rejected the file because it’s too big. Increase the limit in your server config (multer: limits: { fileSize: 50 * 1024 * 1024 }).

“CORS error: Access-Control-Allow-Origin”

The server doesn’t allow cross-origin requests. Ask your backend to add CORS headers or upload to the same domain.

“FormData not sending file”

Make sure you’re using fetch with method: 'POST' and body: formData (not headers: { 'Content-Type': 'application/json' }).

“File appears to upload but isn’t saved”

Check the server logs. The file might be failing validation on the backend. Always log server-side errors.

Next steps

You now understand file uploads in React. Ready to handle forms better?

Go back to the React + VS Code setup guide and explore:

  • Building complete forms with validation
  • Understanding React routing for multi-page file managers
  • Backend integration to actually process uploads

File uploads are often part of larger workflows. Combining them with routing lets you build file management dashboards. Combining with validation lets you build professional forms.

Conclusion

File uploads in React are straightforward once you understand the flow: input → state → FormData → fetch → server. The basics take 20 lines of code. Advanced features (drag-drop, progress, validation) add complexity only when needed.

The key: always validate on the server, never trust the client.

Last updated: January 2026. Information current as of React 19, Node 20 LTS, modern Fetch API.

Picture of Priyanshu Pathak

Priyanshu Pathak

Priyanshu Pathak is a Senior Developer at Sourcebae. He works across the stack to build fast, reliable features that make hiring simple. From APIs and integrations to performance and security, Priyanshu keeps our products clean, scalable, and easy to maintain.

Table of Contents

Hire top 1% global talent now

FAQ

Still Curious? These might help

Related blogs

Quick answer Create routes in React using React Router v6 in 4 steps: That’s it. You now have a multi-page

Quick answer npm (Node Package Manager) is a package manager for JavaScript that manages all the libraries and tools your

Quick answer Install React JS in VS Code in 10 minutes: Your React dev environment is live and hot-reloading. Full

Quick Answer React is primarily a front-end technology. It is a JavaScript library designed for building user interfaces that run