Skip to main content
Back to Blog
Tutorials
4 min read
January 25, 2025

How to Build a File Manager With Drag and Drop in React

Create a file manager component with folder navigation, file uploads, drag-and-drop reordering, context menus, and breadcrumb trails.

Ryel Banfield

Founder & Lead Developer

File managers are complex UI components. Here is how to build one with full navigation and uploads.

Types

export interface FileItem {
  id: string;
  name: string;
  type: "file" | "folder";
  size?: number;
  mimeType?: string;
  parentId: string | null;
  createdAt: string;
  updatedAt: string;
}

export type ViewMode = "grid" | "list";
export type SortBy = "name" | "size" | "updatedAt";

File Manager Hook

"use client";

import { useState, useCallback, useMemo } from "react";
import type { FileItem, SortBy } from "./types";

interface UseFileManagerOptions {
  initialFiles: FileItem[];
  onUpload?: (files: File[], parentId: string | null) => Promise<FileItem[]>;
  onDelete?: (ids: string[]) => Promise<void>;
  onRename?: (id: string, name: string) => Promise<void>;
  onMove?: (ids: string[], targetFolderId: string | null) => Promise<void>;
}

export function useFileManager({
  initialFiles,
  onUpload,
  onDelete,
  onRename,
  onMove,
}: UseFileManagerOptions) {
  const [files, setFiles] = useState<FileItem[]>(initialFiles);
  const [currentFolder, setCurrentFolder] = useState<string | null>(null);
  const [selected, setSelected] = useState<Set<string>>(new Set());
  const [sortBy, setSortBy] = useState<SortBy>("name");
  const [sortAsc, setSortAsc] = useState(true);

  // Current folder contents
  const currentFiles = useMemo(() => {
    const filtered = files.filter((f) => f.parentId === currentFolder);
    return filtered.sort((a, b) => {
      // Folders first
      if (a.type !== b.type) return a.type === "folder" ? -1 : 1;

      let comparison = 0;
      switch (sortBy) {
        case "name":
          comparison = a.name.localeCompare(b.name);
          break;
        case "size":
          comparison = (a.size ?? 0) - (b.size ?? 0);
          break;
        case "updatedAt":
          comparison = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
          break;
      }
      return sortAsc ? comparison : -comparison;
    });
  }, [files, currentFolder, sortBy, sortAsc]);

  // Breadcrumb path
  const breadcrumbs = useMemo(() => {
    const path: FileItem[] = [];
    let folderId = currentFolder;
    while (folderId) {
      const folder = files.find((f) => f.id === folderId);
      if (folder) {
        path.unshift(folder);
        folderId = folder.parentId;
      } else {
        break;
      }
    }
    return path;
  }, [files, currentFolder]);

  const navigateTo = useCallback((folderId: string | null) => {
    setCurrentFolder(folderId);
    setSelected(new Set());
  }, []);

  const toggleSelect = useCallback((id: string, multi: boolean) => {
    setSelected((prev) => {
      const next = multi ? new Set(prev) : new Set<string>();
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }, []);

  const selectAll = useCallback(() => {
    setSelected(new Set(currentFiles.map((f) => f.id)));
  }, [currentFiles]);

  const uploadFiles = useCallback(
    async (fileList: File[]) => {
      if (!onUpload) return;
      const newFiles = await onUpload(fileList, currentFolder);
      setFiles((prev) => [...prev, ...newFiles]);
    },
    [currentFolder, onUpload],
  );

  const deleteSelected = useCallback(async () => {
    if (!onDelete || selected.size === 0) return;
    const ids = Array.from(selected);
    await onDelete(ids);
    setFiles((prev) => prev.filter((f) => !ids.includes(f.id)));
    setSelected(new Set());
  }, [selected, onDelete]);

  const renameFile = useCallback(
    async (id: string, newName: string) => {
      if (!onRename) return;
      await onRename(id, newName);
      setFiles((prev) =>
        prev.map((f) => (f.id === id ? { ...f, name: newName } : f)),
      );
    },
    [onRename],
  );

  const moveFiles = useCallback(
    async (targetFolderId: string | null) => {
      if (!onMove || selected.size === 0) return;
      const ids = Array.from(selected);
      await onMove(ids, targetFolderId);
      setFiles((prev) =>
        prev.map((f) =>
          ids.includes(f.id) ? { ...f, parentId: targetFolderId } : f,
        ),
      );
      setSelected(new Set());
    },
    [selected, onMove],
  );

  const createFolder = useCallback(
    (name: string) => {
      const folder: FileItem = {
        id: crypto.randomUUID(),
        name,
        type: "folder",
        parentId: currentFolder,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };
      setFiles((prev) => [...prev, folder]);
    },
    [currentFolder],
  );

  return {
    files: currentFiles,
    breadcrumbs,
    currentFolder,
    selected,
    sortBy,
    sortAsc,
    navigateTo,
    toggleSelect,
    selectAll,
    uploadFiles,
    deleteSelected,
    renameFile,
    moveFiles,
    createFolder,
    setSortBy,
    setSortAsc,
  };
}

Drop Zone

"use client";

import { useState, useCallback } from "react";

interface DropZoneProps {
  onDrop: (files: File[]) => void;
  children: React.ReactNode;
  className?: string;
}

export function DropZone({ onDrop, children, className }: DropZoneProps) {
  const [dragging, setDragging] = useState(false);
  const [dragCount, setDragCount] = useState(0);

  const handleDragEnter = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    setDragCount((c) => c + 1);
    setDragging(true);
  }, []);

  const handleDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    setDragCount((c) => {
      const next = c - 1;
      if (next === 0) setDragging(false);
      return next;
    });
  }, []);

  const handleDrop = useCallback(
    (e: React.DragEvent) => {
      e.preventDefault();
      setDragging(false);
      setDragCount(0);

      const files = Array.from(e.dataTransfer.files);
      if (files.length > 0) {
        onDrop(files);
      }
    },
    [onDrop],
  );

  return (
    <div
      className={`${className} ${dragging ? "ring-2 ring-primary ring-dashed bg-primary/5" : ""}`}
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDragOver={(e) => e.preventDefault()}
      onDrop={handleDrop}
    >
      {children}
      {dragging && (
        <div className="absolute inset-0 flex items-center justify-center bg-primary/5 z-10 pointer-events-none">
          <p className="text-primary font-medium">Drop files here</p>
        </div>
      )}
    </div>
  );
}

File Grid and List Views

"use client";

import type { FileItem, ViewMode } from "./types";

interface FileListProps {
  files: FileItem[];
  viewMode: ViewMode;
  selected: Set<string>;
  onSelect: (id: string, multi: boolean) => void;
  onOpen: (item: FileItem) => void;
}

export function FileList({
  files,
  viewMode,
  selected,
  onSelect,
  onOpen,
}: FileListProps) {
  if (files.length === 0) {
    return (
      <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
        <p>This folder is empty</p>
        <p className="text-sm">Drop files here or click upload</p>
      </div>
    );
  }

  if (viewMode === "grid") {
    return (
      <div className="grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2 p-4">
        {files.map((file) => (
          <button
            key={file.id}
            className={`flex flex-col items-center gap-1 p-3 rounded-lg text-center hover:bg-muted cursor-pointer ${
              selected.has(file.id) ? "bg-primary/10 ring-1 ring-primary" : ""
            }`}
            onClick={(e) => onSelect(file.id, e.ctrlKey || e.metaKey)}
            onDoubleClick={() => onOpen(file)}
          >
            <span className="text-3xl">
              {file.type === "folder" ? "πŸ“" : getFileIcon(file.mimeType)}
            </span>
            <span className="text-xs truncate w-full">{file.name}</span>
            {file.size && (
              <span className="text-[10px] text-muted-foreground">
                {formatFileSize(file.size)}
              </span>
            )}
          </button>
        ))}
      </div>
    );
  }

  return (
    <table className="w-full text-sm">
      <thead>
        <tr className="border-b text-left text-muted-foreground">
          <th className="px-4 py-2 font-medium">Name</th>
          <th className="px-4 py-2 font-medium">Size</th>
          <th className="px-4 py-2 font-medium">Modified</th>
        </tr>
      </thead>
      <tbody>
        {files.map((file) => (
          <tr
            key={file.id}
            className={`border-b cursor-pointer hover:bg-muted ${
              selected.has(file.id) ? "bg-primary/10" : ""
            }`}
            onClick={(e) => onSelect(file.id, e.ctrlKey || e.metaKey)}
            onDoubleClick={() => onOpen(file)}
          >
            <td className="px-4 py-2 flex items-center gap-2">
              <span>{file.type === "folder" ? "πŸ“" : getFileIcon(file.mimeType)}</span>
              {file.name}
            </td>
            <td className="px-4 py-2 text-muted-foreground">
              {file.size ? formatFileSize(file.size) : "-"}
            </td>
            <td className="px-4 py-2 text-muted-foreground">
              {new Date(file.updatedAt).toLocaleDateString()}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function getFileIcon(mimeType?: string): string {
  if (!mimeType) return "πŸ“„";
  if (mimeType.startsWith("image/")) return "πŸ–ΌοΈ";
  if (mimeType.startsWith("video/")) return "🎬";
  if (mimeType.startsWith("audio/")) return "🎡";
  if (mimeType === "application/pdf") return "πŸ“•";
  return "πŸ“„";
}

function formatFileSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

Need Custom File Management?

We build file management systems tailored to your workflow. Contact us to get started.

file managerdrag and dropReactfile uploadcomponenttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles