Skip to main content
Back to Blog
Tutorials
3 min read
November 13, 2024

How to Build a File Manager Component in React

Build a file manager with folder navigation, upload, grid/list views, and context menus in React.

Ryel Banfield

Founder & Lead Developer

A file manager gives users a familiar way to organize files and folders. Here is how to build one.

Step 1: Types

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

Step 2: File Manager State

"use client";

import { useState, useMemo } from "react";
import type { FileItem } from "@/types/files";

export function useFileManager(initialFiles: FileItem[]) {
  const [files, setFiles] = useState(initialFiles);
  const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [viewMode, setViewMode] = useState<"grid" | "list">("grid");

  const currentFiles = useMemo(
    () => files.filter((f) => f.parentId === currentFolderId),
    [files, currentFolderId]
  );

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

  function navigateTo(folderId: string | null) {
    setCurrentFolderId(folderId);
    setSelectedIds(new Set());
  }

  function toggleSelect(id: string, multi = false) {
    setSelectedIds((prev) => {
      const next = multi ? new Set(prev) : new Set<string>();
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

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

  function deleteSelected() {
    setFiles((prev) => prev.filter((f) => !selectedIds.has(f.id)));
    setSelectedIds(new Set());
  }

  function renameFile(id: string, newName: string) {
    setFiles((prev) =>
      prev.map((f) =>
        f.id === id ? { ...f, name: newName, updatedAt: new Date().toISOString() } : f
      )
    );
  }

  return {
    currentFiles,
    breadcrumbs,
    currentFolderId,
    selectedIds,
    viewMode,
    setViewMode,
    navigateTo,
    toggleSelect,
    createFolder,
    deleteSelected,
    renameFile,
  };
}

Step 3: File Manager Component

"use client";

import { Folder, File, Grid, List, Plus, Trash, ChevronRight } from "lucide-react";
import { useFileManager } from "./useFileManager";
import type { FileItem } from "@/types/files";

export function FileManager({ files }: { files: FileItem[] }) {
  const {
    currentFiles,
    breadcrumbs,
    selectedIds,
    viewMode,
    setViewMode,
    navigateTo,
    toggleSelect,
    createFolder,
    deleteSelected,
  } = useFileManager(files);

  return (
    <div className="rounded-xl border dark:border-gray-700">
      {/* Toolbar */}
      <div className="flex items-center justify-between border-b p-3 dark:border-gray-700">
        <div className="flex items-center gap-2">
          <button
            onClick={() => {
              const name = prompt("Folder name:");
              if (name) createFolder(name);
            }}
            className="flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
          >
            <Plus className="h-4 w-4" /> New Folder
          </button>
          {selectedIds.size > 0 && (
            <button
              onClick={deleteSelected}
              className="flex items-center gap-1 rounded-lg bg-red-600 px-3 py-1.5 text-sm text-white hover:bg-red-700"
            >
              <Trash className="h-4 w-4" /> Delete ({selectedIds.size})
            </button>
          )}
        </div>
        <div className="flex gap-1">
          <button
            onClick={() => setViewMode("grid")}
            className={`rounded-lg p-1.5 ${viewMode === "grid" ? "bg-gray-200 dark:bg-gray-700" : ""}`}
          >
            <Grid className="h-4 w-4" />
          </button>
          <button
            onClick={() => setViewMode("list")}
            className={`rounded-lg p-1.5 ${viewMode === "list" ? "bg-gray-200 dark:bg-gray-700" : ""}`}
          >
            <List className="h-4 w-4" />
          </button>
        </div>
      </div>

      {/* Breadcrumbs */}
      <div className="flex items-center gap-1 border-b px-3 py-2 text-sm dark:border-gray-700">
        <button
          onClick={() => navigateTo(null)}
          className="text-blue-600 hover:underline"
        >
          Home
        </button>
        {breadcrumbs.map((crumb) => (
          <span key={crumb.id} className="flex items-center gap-1">
            <ChevronRight className="h-3 w-3 text-gray-400" />
            <button
              onClick={() => navigateTo(crumb.id)}
              className="text-blue-600 hover:underline"
            >
              {crumb.name}
            </button>
          </span>
        ))}
      </div>

      {/* File listing */}
      <div className={viewMode === "grid"
        ? "grid grid-cols-2 gap-2 p-3 sm:grid-cols-4 md:grid-cols-6"
        : "divide-y p-1 dark:divide-gray-800"
      }>
        {currentFiles.length === 0 ? (
          <p className="col-span-full p-8 text-center text-sm text-gray-500">
            This folder is empty
          </p>
        ) : (
          currentFiles.map((file) =>
            viewMode === "grid" ? (
              <GridItem
                key={file.id}
                file={file}
                selected={selectedIds.has(file.id)}
                onSelect={(multi) => toggleSelect(file.id, multi)}
                onOpen={() => file.type === "folder" && navigateTo(file.id)}
              />
            ) : (
              <ListItem
                key={file.id}
                file={file}
                selected={selectedIds.has(file.id)}
                onSelect={(multi) => toggleSelect(file.id, multi)}
                onOpen={() => file.type === "folder" && navigateTo(file.id)}
              />
            )
          )
        )}
      </div>
    </div>
  );
}

function GridItem({
  file,
  selected,
  onSelect,
  onOpen,
}: {
  file: FileItem;
  selected: boolean;
  onSelect: (multi: boolean) => void;
  onOpen: () => void;
}) {
  return (
    <div
      onClick={(e) => onSelect(e.metaKey || e.ctrlKey)}
      onDoubleClick={onOpen}
      className={`flex cursor-pointer flex-col items-center rounded-lg p-3 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 ${
        selected ? "bg-blue-50 ring-2 ring-blue-500 dark:bg-blue-950" : ""
      }`}
    >
      {file.type === "folder" ? (
        <Folder className="h-10 w-10 text-blue-500" />
      ) : (
        <File className="h-10 w-10 text-gray-400" />
      )}
      <span className="mt-1 max-w-full truncate text-xs">{file.name}</span>
    </div>
  );
}

function ListItem({
  file,
  selected,
  onSelect,
  onOpen,
}: {
  file: FileItem;
  selected: boolean;
  onSelect: (multi: boolean) => void;
  onOpen: () => void;
}) {
  return (
    <div
      onClick={(e) => onSelect(e.metaKey || e.ctrlKey)}
      onDoubleClick={onOpen}
      className={`flex cursor-pointer items-center gap-3 px-3 py-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 ${
        selected ? "bg-blue-50 dark:bg-blue-950" : ""
      }`}
    >
      {file.type === "folder" ? (
        <Folder className="h-5 w-5 text-blue-500" />
      ) : (
        <File className="h-5 w-5 text-gray-400" />
      )}
      <span className="flex-1 text-sm">{file.name}</span>
      {file.size && (
        <span className="text-xs text-gray-400">
          {(file.size / 1024).toFixed(1)} KB
        </span>
      )}
      <span className="text-xs text-gray-400">
        {new Date(file.updatedAt).toLocaleDateString()}
      </span>
    </div>
  );
}

Need a File Management System?

We build web applications with file management, cloud storage integration, and document workflows. Contact us to discuss your project.

file managerfile browserReactuploadtutorial

Ready to Start Your Project?

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

Get in Touch

Related Articles