# React File Manager: Folders, Uploads and the Backend API

> Build a React file manager for SaaS documents: folders in a grid or list, a folder tree, uploads and bulk actions, plus the backend API that keeps it safe.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Nov 6, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/react-file-manager

## TL;DR

A React file manager for a SaaS needs folders in a grid and a list, a folder tree and breadcrumb, uploads with progress, bulk move and delete, keyboard support and a phone layout; most of the hard work is the backend contract behind it. Model the tree as flat rows with an id and a parentId, upload straight to storage through presigned URLs, and let the server refuse moves that would put a folder inside itself. Pick a document manager for a company's or a record's files, and a Finder-style file browser when people browse a large drive and preview what is inside.

Every SaaS that stores documents grows a "Files" tab sooner or later: contracts on a client record, invoices per month, photos from a site visit. A React file manager looks like a grid of icons, but the work is in folders that nest, uploads that fail halfway, a selection of 40 files dragged into another folder, and a phone where none of that can rely on hover or right-click. This guide covers what a file manager UI needs, when a Finder-style file browser is the better fit, the backend contract both depend on, and the open source options. The examples use the [File Manager](https://wingo-ui.com/components/file-manager) and [File System](https://wingo-ui.com/components/file-system) components from Wingo UI, the library we build, so we are not neutral; facts about other projects link to their sources.

## What should a React file manager do in a SaaS app?

Here is the list we built our React file manager UI against. It is also a fair test for any package or a hand-written version:

- **Folders in two layouts.** A grid with thumbnails for photos and a list with Modified, Size and Owner columns for documents, with a switch between them that remembers the choice.
- **Where am I.** A breadcrumb whose crumbs are drop targets, and a folder tree beside the files when there is room.
- **Selection and bulk actions.** Click, Cmd or Ctrl + click, Shift + click for a range, a checkbox per item, and a toolbar that turns into Move, Download and Delete once something is selected.
- **Uploads.** A button, drag and drop from the desktop into the open folder or onto a folder tile, paste, a progress row per file, and rejected files listed with a reason.
- **Rename, move, delete.** Rename in place with an error under the field when the server refuses, move by drag or a dialog, delete with a confirmation that names what goes.
- **Search across folders**, with each result's folder path.
- **Keyboard and touch.** Arrows, Enter, Delete, F2 and Backspace on a desktop; tap, long press and bottom sheets on a phone.

Install the component:

```bash
npx wingo-ui@latest add file-manager
```

It brings its parts along (tree view, file upload, breadcrumb, context menu, lightbox and dialogs), because a file manager UI is a composition of those. Try the demo below: it keeps the files in memory, so you can upload, rename, move and delete freely.

> Live demo (File manager): Select two files with Cmd or Ctrl + click and drag them onto a folder tile or a crumb in the breadcrumb, then press F2 on a file to rename it in place. Try it at [File manager](https://wingo-ui.com/components/file-manager) and install it with `npx wingo-ui@latest add file-manager`.

## File manager or Finder-style file browser: which one do you need?

Pick a file manager when the files belong to a record and people come to drop one in or fetch one. Pick a Finder-style file browser when the files are the page: people spend time in it, move between many folders and want to look inside files without downloading them. Wingo UI has one component for each shape: File Manager for the first, and File System, a Finder-style React file browser component, for the second.

| | File Manager (`file-manager`) | File System (`file-system`) |
|---|---|---|
| Typical place | A "Documents" tab on a record or a company | A drive or media library screen of its own |
| Views | Grid and list | Icons, list, columns and gallery |
| Navigation | Breadcrumb, folder tree, search | Sidebar with favorites, locations and tags, back and forward, path bar, Go to palette |
| Previews | Images in a lightbox | Quick Look on PDF, Word, Excel, CSV, PowerPoint, images, video, audio, Markdown, code |
| Data | Flat items with parentId | Nested children, flat parentId, or lazy `loadChildren` per folder |
| Extras | Quota bar, uploads panel, owner column | Copy and duplicate, tags, Get info, a 10,000 file folder stays virtualized |

The rule we use: a file manager for "the files that belong to this thing", a file browser for "a place where files live". If people mostly upload and download, the browser's extra views only add weight.

> Live demo (File System): Press Space on a file to open Quick Look, then switch to the Columns view with Cmd or Ctrl + 3. Try it at [File System](https://wingo-ui.com/components/file-system) and install it with `npx wingo-ui@latest add file-system`.

## How do I connect a React file manager to my data?

Pass every folder and file as one flat `items` array and turn each action on by passing its handler. Each item is `{ id, kind, name, parentId, size, type, url, modifiedAt, modifiedBy }`, and the component works out folders, the tree and the breadcrumb from the `parentId` links. `onUpload` adds the Upload button and drag and drop, `onMove` adds dragging onto folders and the Move dialog, `onRename`, `onDelete` and `onCreateFolder` add the rest. Leave them all out and it is a read-only viewer.

Two details make the error states work. A handler that returns a rejected promise keeps its dialog or rename field open with the error message, so a duplicate name or a lost connection shows where the person is looking. And upload progress is yours to report through the `uploads` prop, which feeds the floating uploads panel.

```tsx
"use client";

import { useState } from "react";
import { FileManager, type FileManagerItem, type FileManagerUpload } from "@/components/ui/file-manager";

async function api<T>(url: string, init?: RequestInit): Promise<T> {
  const res = await fetch(url, { ...init, headers: { "Content-Type": "application/json" } });
  if (!res.ok) {
    const body = await res.json().catch(() => null);
    throw new Error(body?.message ?? `Request failed (${res.status})`);
  }
  // rename, move and delete may answer 204 with no body
  return (res.status === 204 ? undefined : await res.json()) as T;
}

// xhr, because fetch has no upload progress event
function put(url: string, file: File, type: string, onProgress: (progress: number) => void) {
  return new Promise<void>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress(event.loaded / event.total);
    };
    xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`Upload failed (${xhr.status})`)));
    xhr.onerror = () => reject(new Error("Network error"));
    xhr.open("PUT", url);
    // must match the Content-Type the URL was signed with
    xhr.setRequestHeader("Content-Type", type);
    xhr.send(file);
  });
}

export function ClientDocuments({ clientId, initial }: { clientId: string; initial: FileManagerItem[] }) {
  const [items, setItems] = useState(initial);
  const [uploads, setUploads] = useState<FileManagerUpload[]>([]);
  const base = `/api/clients/${clientId}/files`;

  async function uploadOne(file: File, folderId: string | null) {
    const id = crypto.randomUUID();
    const type = file.type || "application/octet-stream";
    const patch = (next: Partial<FileManagerUpload>) =>
      setUploads((list) => list.map((upload) => (upload.id === id ? { ...upload, ...next } : upload)));
    setUploads((list) => [...list, { id, name: file.name, size: file.size, progress: 0, status: "uploading", folderId }]);
    try {
      const { uploadUrl, fileId } = await api<{ uploadUrl: string; fileId: string }>(`${base}/uploads`, {
        method: "POST",
        body: JSON.stringify({ name: file.name, size: file.size, type, folderId }),
      });
      await put(uploadUrl, file, type, (progress) => patch({ progress }));
      const saved = await api<FileManagerItem>(`${base}/${fileId}/complete`, { method: "POST" });
      setItems((list) => [...list, saved]);
      patch({ progress: 1, status: "done" });
    } catch (error) {
      patch({ status: "error", error: error instanceof Error ? error.message : "Upload failed" });
    }
  }

  return (
    <FileManager
      items={items}
      uploads={uploads}
      rootLabel="Client files"
      persistKey={`client-files:${clientId}`}
      accept="image/*,.pdf,.docx,.xlsx"
      maxFileSize={50 * 1024 * 1024}
      onUpload={(files, folderId) => Promise.all(files.map((file) => uploadOne(file, folderId)))}
      onCreateFolder={async (name, parentId) => {
        const folder = await api<FileManagerItem>(`${base}/folders`, { method: "POST", body: JSON.stringify({ name, parentId }) });
        setItems((list) => [...list, folder]);
      }}
      onRename={async (item, name) => {
        await api(`${base}/${item.id}`, { method: "PATCH", body: JSON.stringify({ name }) });
        setItems((list) => list.map((i) => (i.id === item.id ? { ...i, name } : i)));
      }}
      onMove={async (ids, parentId) => {
        await api(`${base}/move`, { method: "POST", body: JSON.stringify({ ids, parentId }) });
        setItems((list) => list.map((i) => (ids.includes(i.id) ? { ...i, parentId } : i)));
      }}
      onDelete={async (ids) => {
        await api(base, { method: "DELETE", body: JSON.stringify({ ids }) });
        setItems((list) => list.filter((i) => !ids.includes(i.id)));
      }}
    />
  );
}
```

`accept` and `maxFileSize` reject files before `onUpload` runs, and the uploads panel lists them with the reason. Your server still checks both, since any client can call the endpoint. The upload flow itself (drag and drop, paste, progress, cancel) is covered in depth in our [React file upload guide](https://wingo-ui.com/blog/react-file-upload), and the standalone dropzone is the [File Upload component](https://wingo-ui.com/components/file-upload).

## What backend contract does a file manager need?

A file manager needs seven endpoints (list, new folder, rename, move, delete, upload, download), keyed by stable ids and checked on the server. That holds for whatever React file manager you choose, open source or not:

| Action | Request | Prop it serves |
|---|---|---|
| List | `GET /files` (or per folder) returns rows with `id`, `parentId`, `name`, `kind` | `items`, or `loadChildren` in File System |
| New folder | `POST /files/folders { name, parentId }` returns the row | `onCreateFolder` |
| Rename | `PATCH /files/:id { name }` | `onRename` |
| Move | `POST /files/move { ids, parentId }` | `onMove` |
| Delete | `DELETE /files { ids }` | `onDelete` |
| Upload | `POST /files/uploads` returns a presigned URL; `POST /files/:id/complete` creates the row | `onUpload` + `uploads` |
| Download | `GET /files/:id/download` redirects to a short-lived storage URL | `url`, `onDownload`, or `resolveUrl` in File System |

The rules that keep it correct:

1. **Ids, not paths.** Store a stable id and a `parentId`. A path like `/Contracts/2026/acme.pdf` as the key means a folder rename rewrites every row under it, and every shared link breaks.
2. **Authorize every id.** Each request checks that every id in it belongs to the tenant and record in the URL. A bulk delete with one foreign id should fail as a whole.
3. **Name conflicts are the server's call.** Two people can create "Invoices" in the same folder at the same moment. Pick a rule (refuse with a clear message, or add " (2)") and return the error text, which the dialog shows.
4. **Storage keys are not file names.** S3 replaces an existing object when an upload uses the same key, per the [AWS presigned upload docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html) (checked November 6, 2026). Use the file id as the key and keep the name in your database. Those docs also say that when the URL is signed with a Content-Type, the upload must send the same one, which is why the example signs and sends one value.
5. **Upload around your functions.** Vercel Functions accept request bodies up to 4.5 MB and return 413 above that, per [Vercel's limits page](https://vercel.com/docs/functions/limitations) (as of November 6, 2026). Presigned URLs send the bytes straight to storage.
6. **Soft delete first.** Mark rows deleted and purge later. People delete the wrong folder, and a folder delete is a whole subtree.
7. **Refuse cycles on the server.** The UI already blocks dropping a folder into itself or a subfolder, but the move endpoint must check again:

```ts
type FolderRow = { id: string; parentId: string | null };

// false when the target is one of the moved folders or sits inside one
export function canMove(folders: FolderRow[], ids: string[], targetId: string | null) {
  const parentOf = new Map(folders.map((folder) => [folder.id, folder.parentId]));
  const moving = new Set(ids);
  const seen = new Set<string>();
  for (let at: string | null = targetId; at !== null; at = parentOf.get(at) ?? null) {
    if (moving.has(at) || seen.has(at)) return false;
    seen.add(at);
  }
  return true;
}
```

Run it in the same transaction as the update, after locking the tenant's folder rows (`SELECT ... FOR UPDATE` in Postgres) or under serializable isolation. A plain transaction at the default isolation level still lets two concurrent moves, A into B and B into A, each pass the check and build a loop. The `seen` set also stops the walk if bad data already contains one.

## How does a file manager work on a phone?

On a phone the File Manager swaps hover, right-click and drag for taps, long presses and bottom sheets. It starts as a list there, with the folder name as a large title and a back link to the parent in place of the full breadcrumb. A tap opens, a long press starts selecting (checkboxes appear and a floating bar offers Move, Download and Delete), and every menu, the Move dialog and the New folder dialog open as bottom sheets. The layout follows the component's own width through container queries, so the same compact list appears in a narrow side panel on a wide screen, and the folder tree only shows once the manager itself is 768px wide.

For accessibility, the files are a single Tab stop with arrow key navigation inside, letters jump to a name, Space toggles selection, Shift + F10 opens the item menu and Delete asks before removing. The menu a right-click opens is also on a ⋯ button per item, which shows on hover, on keyboard focus and on selection, and stays visible on touch screens, so nothing depends on a mouse.

## Is there a good open source React file manager?

Yes, and for many projects a free package is the right answer. Two MIT licensed projects are worth a look as of November 6, 2026 (star counts from GitHub on that date):

| Project | License | Data model | Notes |
|---|---|---|---|
| [@cubone/react-file-manager](https://github.com/Saifullah-dev/react-file-manager) | MIT | `files` array of `{ name, isDirectory, path }` | Grid and list, `fileUploadConfig` posts to your endpoint, `permissions` flags, an example Node and MongoDB backend; 158 GitHub stars |
| [SVAR React File Manager](https://svar.dev/react/filemanager/) | MIT | `data` array with path ids like `/Music` | List, tiles and a split view, a tree and breadcrumbs, folders loaded as people open them, light and dark themes; 87 GitHub stars ([repo](https://github.com/svar-widgets/react-filemanager)) |
| [Wingo UI File Manager](https://wingo-ui.com/components/file-manager) | Pro, source copied into your repo | Flat `items` with `parentId` | Grid and list, tree, uploads panel, bulk actions, phone layout with sheets, `UIProvider` defaults |

Pick @cubone or SVAR when you want a free, drop-in React file explorer and are happy with its look and its path-based ids. Pick a source component when the file manager has to match the rest of your design system and you expect to change its markup: you own the file, and `npx wingo-ui@latest update` merges our later fixes into your edits. Wingo UI is a paid library: Pro is $8 a month, $80 a year or $150 once, see [pricing](https://wingo-ui.com/pricing). The 14-day refund applies only while you have not downloaded a Pro item.

## When is a tree view enough?

A tree view is enough when people pick or arrange folders and never work with the files inside them: a settings screen that picks a destination folder, a sidebar of project sections, or a code sample browser. The [Tree View](https://wingo-ui.com/components/tree-view) is the same component the file manager uses for its folders: lazy children through `loadChildren`, drag to move with `draggable` and `onMove`, filtering and folder icons. Our [React tree view guide](https://wingo-ui.com/blog/react-tree-view) walks through it, and the [component guides category](https://wingo-ui.com/blog/category/components) has the rest of this series, with the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) as its overview.

## Next step

Open the [File Manager docs](https://wingo-ui.com/components/file-manager), change the view, tile size and tone in the playground, and copy the usage code into a page with your own `items`. Then write the seven endpoints above before you style anything, since that is where file features break in production. Both file components are Pro; the docs, the playground and the [MCP server](https://wingo-ui.com/docs/mcp) work without an account.

## Components in this post

- [File manager](https://wingo-ui.com/components/file-manager) (Pro): A React file manager for the documents of a company or a record: folders and files in a grid or a list, a folder tree, uploads and bulk actions. Install: `npx wingo-ui@latest add file-manager`
- [File System](https://wingo-ui.com/components/file-system) (Pro): A React file browser in the style of Finder: a sidebar, icons, list, columns and gallery views, a preview panel and Quick Look on real documents. Install: `npx wingo-ui@latest add file-system`
- [Tree View](https://wingo-ui.com/components/tree-view) (Pro): A React tree view for folders, categories or permissions: animated expand, selection, tri-state checkboxes, filter and drag to move. Install: `npx wingo-ui@latest add tree-view`
- [File Upload](https://wingo-ui.com/components/file-upload) (Pro): A React file upload with drag and drop, paste and camera capture: validation, previews, per-file progress, and list, grid or avatar layouts. Install: `npx wingo-ui@latest add file-upload`

## FAQ

### What is the difference between a file manager and a file explorer in React?

The words are used for the same thing, a UI that lists folders and files and lets people open, move, rename and upload them. In practice there are two shapes: a document manager embedded in an app page (grid or list, a tree, bulk actions) and a desktop-style file browser with a sidebar, several views and previews. Choose by how people use the files, not by the name.

### Is there a free open source React file manager?

Yes. As of November 6, 2026, @cubone/react-file-manager and SVAR React File Manager (@svar-ui/react-filemanager) are both MIT licensed and published on GitHub. Both leave storage to you through callbacks or a data prop, so you still write the backend.

### How should I store folders for a React file manager?

Store every folder and file as a row with a stable id and a parentId (null at the root), and keep the name as a plain column. A rename then changes one row, a move changes one parentId, and links to a file survive both.

### How do I upload large files from a React file manager?

Ask your API for a presigned storage URL, send the file from the browser straight to storage with XMLHttpRequest so you get progress events, then confirm the upload so the server creates the file row. That keeps the bytes away from serverless functions, which Vercel caps at 4.5 MB per request body.

### How do I stop a folder from being moved into its own subfolder?

Walk up from the target folder through its parents; if you reach one of the folders being moved, refuse the move. The UI can block it early, but the server must check it too, because any client can call your move endpoint.

---

Source: https://wingo-ui.com/blog/react-file-manager
