DEV Community

Cover image for Creating a Drag & Drop File Uploader in React (Next.js)
Arpit
Arpit

Posted on • Originally published at codesnail.com

Creating a Drag & Drop File Uploader in React (Next.js)

While working on my side project, NotchTools.com, I needed a drag-and-drop file uploader for Image Tools. I'm using DaisyUI, a Tailwind-based component library, for styling throughout the project. However, DaisyUI does not provide a pre-built file upload component with drag-and-drop functionality. So, I decided to create my own sharable and reusable component that fits seamlessly with the Tailwind/DaisyUI design philosophy.

Key Features:

  • Drag & Drop Support: Allows users to upload files by dragging them into the upload area.
  • Customizable File Acceptance: You can specify the types of files allowed, whether to support single or multiple uploads.
  • Error Handling: Proper error messages are displayed for invalid file types or exceeding the file limit.

Component Props:

  • onFileSelect: A callback function that triggers when files are selected either by dragging or clicking to upload.
  • multiple: A boolean flag (optional, defaults to false) to allow multiple file uploads.
  • accept: A string (optional) specifying the accepted file types (e.g., image/*).
  • className: Optional custom classes for styling the main upload area.

Code Walkthrough:

1. State Management:

The component uses two key states:

  • dragActive: A boolean that tracks whether a drag event is active (i.e., when a user is dragging a file over the upload area).
  • error: Stores error messages related to invalid file types or if too many files are uploaded.
const [dragActive, setDragActive] = useState(false);
const [error, setError] = useState<string | null>(null);
Enter fullscreen mode Exit fullscreen mode

2. Drag Events Handling:

For the drag-and-drop functionality, the component listens to the following drag events:

  • onDragEnter and onDragOver: To detect when the file is being dragged over the area.
  • onDragLeave: To reset the state when the file is dragged out of the area.
  • onDrop: To handle the actual file drop and trigger validation.

The handleDrag function toggles the dragActive state based on the event type to visually update the component.

const handleDrag = (e: React.DragEvent) => {
  e.preventDefault();
  e.stopPropagation();
  setDragActive(e.type === "dragenter" || e.type === "dragover");
};
Enter fullscreen mode Exit fullscreen mode

3. File Drop Validation:

When files are dropped, the handleDrop function runs several validations:

  • Multiple File Check: If multiple is false, the component only allows one file to be uploaded at a time.
  • File Type Validation: Checks whether the dropped file(s) match the accepted file types using the accept prop.
const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();
  e.stopPropagation();
  setDragActive(false);

  const files = e.dataTransfer.files;

  if (!files.length) return;

  if (!multiple && files.length > 1) {
    setError("Only one file is allowed");
    return;
  }

  const acceptedTypes = accept ? accept.split(",") : [];
  for (let i = 0; i < files.length; i++) {
    const file = files[i];

    const isValidFileType = acceptedTypes.some((type) => {
      const regex = new RegExp(type.replace("*", ".*"));
      return regex.test(file.type);
    });

    if (accept && !isValidFileType) {
      setError(`Invalid file type: ${file.name}`);
      return;
    }
  }

  setError(null);
  onFileSelect(files);
};
Enter fullscreen mode Exit fullscreen mode

4. Fallback for File Input:

For users who prefer to upload files by clicking, a hidden file input field is provided. The triggerFileSelect function programmatically opens the file dialog when the upload area is clicked.

const triggerFileSelect = () => {
  inputRef.current?.click();
};
Enter fullscreen mode Exit fullscreen mode

5. Tailwind & DaisyUI:

The drop area is styled using DaisyUI classes. The dragActive state changes the border color when a file is being dragged over the area.

<div
  onDragEnter={handleDrag}
  onDragOver={handleDrag}
  onDragLeave={handleDrag}
  onDrop={handleDrop}
  className={`${className} border-2 border-dashed p-6 rounded-lg text-center cursor-pointer transition-colors ${
    dragActive ? "border-primary" : "border-base-50 bg-base-200"
  }`}
  onClick={triggerFileSelect}
>
  <input
    ref={inputRef}
    type="file"
    className="hidden"
    multiple={multiple}
    accept={accept}
    onChange={handleChange}
  />
  <p className="text-base-content/50">
    {multiple
      ? "Drag & Drop files here or click to upload multiple files"
      : "Drag & Drop a file here or click to upload"}
  </p>
</div>
Enter fullscreen mode Exit fullscreen mode

6. Error Messaging:

If there is an issue with the uploaded files (e.g., invalid type), an error message is displayed beneath the upload area.

{
  error && <p className="text-xs text-error mt-2">{error}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Full Component Code:

Here I'm using next.js for the project.

"use client";

import React, { useState, useRef } from "react";

interface FileUploadProps {
  onFileSelect: (files: FileList) => void;
  multiple?: boolean;
  accept?: string;
  className?: string;
}

const FileUpload: React.FC<FileUploadProps> = ({
  onFileSelect,
  multiple = false,
  accept,
  className,
}) => {
  const [dragActive, setDragActive] = useState(false);
  const inputRef = useRef<HTMLInputElement | null>(null);
  const [error, setError] = useState<string | null>(null);

  const handleDrag = (e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(e.type === "dragenter" || e.type === "dragover");
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(false);

    const files = e.dataTransfer.files;

    // Check if any files are dropped
    if (!files.length) {
      return;
    }

    // If multiple is false and more than one file is dropped
    if (!multiple && files.length > 1) {
      setError("Only one file is allowed");
      return;
    }

    // Validate file types
    const acceptedTypes = accept ? accept.split(",") : [];
    for (let i = 0; i < files.length; i++) {
      const file = files[i];

      // Check if the file type matches the accept prop (using regex or exact match)
      const isValidFileType = acceptedTypes.some((type) => {
        const regex = new RegExp(type.replace("*", ".*")); // Convert 'image/*' to 'image/.*'
        return regex.test(file.type);
      });

      if (accept && !isValidFileType) {
        setError(`Invalid file type: ${file.name}`);
        return;
      }
    }

    // If everything is valid, reset error and call the onFileSelect function
    setError(null);
    onFileSelect(files);
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      onFileSelect(e.target.files);
    }
  };

  const triggerFileSelect = () => {
    inputRef.current?.click();
  };

  return (
    <>
      <div
        onDragEnter={handleDrag}
        onDragOver={handleDrag}
        onDragLeave={handleDrag}
        onDrop={handleDrop}
        className={`${className} border-2 border-dashed p-6 rounded-lg text-center cursor-pointer transition-colors ${
          dragActive ? "border-primary" : "border-base-50 bg-base-200"
        }`}
        onClick={triggerFileSelect}
      >
        <input
          ref={inputRef}
          type="file"
          className="hidden"
          multiple={multiple}
          accept={accept}
          onChange={handleChange}
        />
        <p className="text-base-content/50">
          {multiple
            ? "Drag & Drop files here or click to upload multiple files"
            : "Drag & Drop a file here or click to upload"}
        </p>

        {/* {accept && (
        <p className="text-xs text-gray-400 mt-2">
          Accepted file types: {accept}
        </p>
      )} */}
      </div>
      {error && <p className="text-xs text-error mt-2">{error}</p>}
    </>
  );
};

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

Thank you :)

Top comments (0)