Drag and drop file upload functionality enhances user experience by providing an intuitive way to handle files in web applications. In this post, we use the well-supported library react-dropzone to build a robust, type-safe file upload component in React.

Setting up the project

Begin by installing the required dependency using either npm or yarn:

npm install react-dropzone
# Or
yarn add react-dropzone

Creating a TypeScript file upload component

Below is a complete example of a drag and drop file upload component implemented with React and TypeScript. This component leverages react-dropzone to manage file selection, validation, and error handling.

import React, { useCallback, useState } from 'react'
import { useDropzone, FileRejection } from 'react-dropzone'

interface UploadProps {
  onUploadComplete?: (files: File[]) => void
  maxFiles?: number
  maxSize?: number
}

const DragAndDropUpload: React.FC<UploadProps> = ({
  onUploadComplete,
  maxFiles = 1,
  maxSize = 5242880, // 5MB
}) => {
  const [files, setFiles] = useState<File[]>([])
  const [uploadError, setUploadError] = useState<string | null>(null)

  const onDropAccepted = useCallback((acceptedFiles: File[]) => {
    setFiles(acceptedFiles)
    setUploadError(null)
    if (onUploadComplete) {
      onUploadComplete(acceptedFiles)
    }
  }, [onUploadComplete])

  const onDropRejected = useCallback((fileRejections: FileRejection[]) => {
    const errors = fileRejections.map(
      (rejection) => `${rejection.file.name}: ${rejection.errors[0].message}`
    )
    setUploadError(errors.join('\n'))
  }, [])

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDropAccepted,
    onDropRejected,
    maxFiles,
    maxSize,
    accept: {
      'image/*': ['.jpeg', '.jpg', '.png', '.gif'],
      'application/pdf': ['.pdf'],
    },
  })

  return (
    <div className="upload-container">
      <div
        {...getRootProps()}
        className={`dropzone ${isDragActive ? 'active' : ''}`}
      >
        <input {...getInputProps()} />
        {isDragActive ? (
          <p>Drop the files here</p>
        ) : (
          <p>Drag and drop files here, or click to select files</p>
        )}
      </div>

      {uploadError && <div className="error-message">{uploadError}</div>}

      {files.length > 0 && (
        <div className="file-list">
          <h4>Selected Files:</h4>
          <ul>
            {files.map((file) => (
              <li key={file.name}>
                {file.name} - {(file.size / 1024).toFixed(2)} KB
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}

export default DragAndDropUpload

Handling file uploads

This function demonstrates how to handle file uploads using the Fetch API along with proper error handling. It converts the selected files into a FormData object and sends them to your back-end API endpoint.

const uploadFiles = async (files: File[]): Promise<void> => {
  const formData = new FormData()
  files.forEach((file) => {
    formData.append('files', file)
  })

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

    if (!response.ok) {
      const errorData = await response.json()
      throw new Error(errorData.message || 'Upload failed')
    }

    const result = await response.json()
    console.log('Upload successful:', result)
  } catch (error: any) {
    console.error('Upload error:', error)
    throw error
  }
}

Styling the component

The following CSS styles help create a clean and responsive design for the upload component. Adjust these styles as needed for your application.

.upload-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.dropzone {
  border: 2px dashed #0087f7;
  border-radius: 4px;
  padding: 20px;
  text-align: center;
  background: #f8f9fa;
  cursor: pointer;
  transition: all 0.3s ease;
}

.dropzone.active {
  border-color: #00e676;
  background: #e3f2fd;
}

.error-message {
  color: #d32f2f;
  margin-top: 10px;
  padding: 10px;
  background: #ffebee;
  border-radius: 4px;
}

.file-list {
  margin-top: 20px;
}

.file-list ul {
  list-style: none;
  padding: 0;
}

.file-list li {
  padding: 8px;
  background: #f5f5f5;
  margin-bottom: 8px;
  border-radius: 4px;
}

Advanced file uploads with Uppy

For projects that require advanced features such as upload progress tracking, chunked uploads, or resumable uploads, Uppy is an excellent alternative. The snippet below demonstrates how to integrate Uppy (version 4.x) with React.

import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import '@uppy/core/dist/style.min.css'
import '@uppy/dashboard/dist/style.min.css'

const uppy = new Uppy()

function UppyUploader() {
  return <Dashboard uppy={uppy} />
}

export default UppyUploader

Uppy builds on modern web standards and works seamlessly with React, making it a powerful choice for handling complex file uploads.

Accessibility and mobile considerations

When implementing drag and drop file uploads, consider the following to ensure a great user experience across all devices:

  • Ensure the dropzone is keyboard accessible by adding focus styles or attributes if needed.
  • Use clear and concise instructions so users understand how to interact with the component.
  • Test the dropzone on mobile devices to verify that fallback behaviors (such as file selection via taps) work reliably.
  • Consider adding ARIA attributes, such as aria-label, to improve accessibility for screen readers.

Usage example

Integrate the DragAndDropUpload component into your application as shown below. In this example, the parent component handles file uploads by invoking the uploadFiles function upon completion of the file selection.

import React from 'react'
import DragAndDropUpload from './DragAndDropUpload'

const App: React.FC = () => {
  const handleUploadComplete = async (files: File[]) => {
    try {
      await uploadFiles(files)
      // Additional success handling can go here
    } catch (error) {
      console.error('Upload failed:', error)
    }
  }

  return (
    <div>
      <h1>File Upload</h1>
      <DragAndDropUpload
        onUploadComplete={handleUploadComplete}
        maxFiles={5}
        maxSize={5242880}
      />
    </div>
  )
}

export default App

Conclusion

In this post, we built a robust, type-safe drag and drop file upload component using react-dropzone. We covered setting up the project, implementing file validation and error handling, and styling the component. Additionally, we explored how Uppy can be integrated for advanced upload features and offered tips for ensuring accessibility and mobile friendliness.

For more advanced features like upload progress tracking, chunked uploads, and resumable uploads, consider using Uppy—a powerful, open source file uploader developed by Transloadit (https://uppy.io).