DEV Community

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

Posted on • Originally published at codesnail.com

2

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 :)

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)