Building a file explorer in React

Written by Akash Sharma on Jul 28 2025


In this article we'll discuss how I solved the commonly asked question of building a file explorer in React. To understand the implementation better, I've created a live demo that you can interact with directly. Try creating folders and files, collapsing and expanding folders, editing names and deleting items.

📂 Files📄📂
📄 notes.txt✏️🗑️
📄 todo.txt✏️🗑️
📂 Images✏️📄📂🗑️
📄 image1.png✏️🗑️
📄 image2.png✏️🗑️
📂 Videos✏️📄📂🗑️
📄 video1.mp4✏️🗑️
📄 video2.mp4✏️🗑️

Now lets discuss the thought process behind this implementation.

The first thing that had to be dealt with was the data structure. I naturally chose to use a tree data structure where each folder can have multiple children, which can be either files or other folders. This allows for a flexible and expandable file system. The root of this structure is a Folder object, which can contain other Folder or File objects.

The Folder and File classes are defined as follows:

class Folder {
    name;
    id;
    parent;
    children = [];
    isRoot = false;
    expanded = true;
}

class File {
    name;
    id;
    parent;
}

In the main app component, I initialize the file explorer with a root folder. The state is managed using React's useState hook, allowing for dynamic updates to the file structure.

To render the file explorer, I created a recursive function that traverses the tree structure using Pre-order traversal. This function checks if the current item is a folder or a file and renders it accordingly. For folders, it also handles the expand/collapse functionality. It also renders the icon, name, collapse indicator, and a toolbar with options to create new files or folders, edit names, and delete items.

For the toolbar, I created a separate component that provides buttons for creating new files or folders, editing names, and deleting items. The toolbar is conditionally rendered based on whether the item is a folder or a file. It also accepts a callback functions to act when an item is clicked.

Whenever a new file or folder is created or name is edited, the browser's prompting system is used to get the name from the user. The new item is then added to the current folder's children array, and the state is updated to reflect this change.

Thank you for reading 😃.

Attaching the entire code here.

import { useState } from 'react';
import './index.css';

class File {
  name;
  id;
  parent;

  constructor({ name, parent = null }) {
    this.id = crypto.randomUUID();
    this.name = name;
    this.parent = parent;
  }

  editName() {
    const fileName = prompt();
    if (fileName) {
      this.name = fileName;
    }
  }

  remove() {
    this.parent.children = this.parent.children.filter((e) => e.id != this.id);
  }
}

class Folder {
  name;
  id;
  parent;
  children = [];
  isRoot = false;
  expanded = true;

  constructor({ name, isRoot = false, parent = null }) {
    this.id = crypto.randomUUID();
    this.name = name;
    this.isRoot = isRoot;
    this.parent = parent;
  }

  createNewFile() {
    const fileName = prompt();
    if (fileName) {
      const file = new File({ name: fileName });
      this.children.push(file);
      file.parent = this;
    }
  }

  createNewFolder() {
    const folderName = prompt();
    if (folderName) {
      const folder = new Folder({ name: folderName });
      this.children.push(folder);
      folder.parent = this;
    }
  }

  editName() {
    const folderName = prompt();
    if (folderName) {
      this.name = folderName;
    }
  }

  remove() {
    this.parent.children = this.parent.children.filter((e) => e.id != this.id);
  }

  toggleExpanded() {
    this.expanded = !this.expanded;
  }
}

function Toolbar({
  showEdit = true,
  showNewFile = true,
  showNewFolder = true,
  showTrash = true,
  onClickEdit = () => {},
  onClickNewFile = () => {},
  onClickNewFolder = () => {},
  onClickTrash = () => {},
}) {
  return (
    <span className='toolbar'>
      {showEdit ? <span onClick={onClickEdit}>✏️</span> : null}
      {showNewFile ? <span onClick={onClickNewFile}>📄</span> : null}
      {showNewFolder ? <span onClick={onClickNewFolder}>📂</span> : null}
      {showTrash ? <span onClick={onClickTrash}>🗑️</span> : null}
    </span>
  );
}

const getInitialState = () => {
  const rootFolder = new Folder({ name: 'Files', isRoot: true });
  const file1 = new File({ name: 'notes.txt', parent: rootFolder });
  rootFolder.children.push(file1);
  const file2 = new File({ name: 'todo.txt', parent: rootFolder });
  rootFolder.children.push(file2);
  const subFolder1 = new Folder({ name: 'Images', parent: rootFolder });
  rootFolder.children.push(subFolder1);
  const subFile1 = new File({ name: 'image1.png', parent: subFolder1 });
  subFolder1.children.push(subFile1);
  const subFile2 = new File({ name: 'image2.png', parent: subFolder1 });
  subFolder1.children.push(subFile2);
  const subFolder2 = new Folder({ name: 'Videos', parent: rootFolder });
  rootFolder.children.push(subFolder2);
  const subFile3 = new File({ name: 'video1.mp4', parent: subFolder2 });
  subFolder2.children.push(subFile3);
  const subFile4 = new File({ name: 'video2.mp4', parent: subFolder2 });
  subFolder2.children.push(subFile4);
  return rootFolder;
};

export default function App() {
  const [explorer] = useState(getInitialState());
  const [, setCounter] = useState(0);

  function initFileCreation(folder) {
    folder.createNewFile();
    setCounter((x) => x + 1);
  }

  function initFolderCreation(folder) {
    folder.createNewFolder();
    setCounter((x) => x + 1);
  }

  function initEdit(entity) {
    entity.editName();
    setCounter((x) => x + 1);
  }

  function initDelete(entity) {
    entity.remove();
    setCounter((x) => x + 1);
  }

  function expandFolder(folder) {
    folder.toggleExpanded();
    setCounter((x) => x + 1);
  }

  function traverseExplorer(root, elementList, level) {
    if (root instanceof File) {
      elementList.push(
        <div
          key={root.id}
          className='itemContainer'
          style={{ marginLeft: `${15 * level}px` }}
        >
          <span className='itemTitle' title={root.name}>📄 {root.name}</span>
          <Toolbar
            {...{
              showNewFile: false,
              showNewFolder: false,
              onClickEdit: () => initEdit(root),
              onClickTrash: () => initDelete(root),
            }}
          />
        </div>
      );
      return;
    }
    if (root instanceof Folder) {
      elementList.push(
        <div
          key={root.id}
          className='itemContainer'
          style={{ marginLeft: `${15 * level}px` }}
        >
          <span className='itemTitle' title={root.name} onClick={() => expandFolder(root)}>
            <span className={`arrow ${root.expanded ? 'arrowExpanded' : ''}`}>
            </span>{' '}
            📂 {root.name}
          </span>
          <Toolbar
            {...{
              showEdit: !root.isRoot,
              showTrash: !root.isRoot,
              onClickNewFile: () => initFileCreation(root),
              onClickNewFolder: () => initFolderCreation(root),
              onClickEdit: () => initEdit(root),
              onClickTrash: () => initDelete(root),
            }}
          />
        </div>
      );
      if (root.children.length === 0 || !root.expanded) {
        return;
      } else {
        for (const e of root.children) {
          traverseExplorer(e, elementList, level + 1);
        }
      }
    }
  }

  function parseExplorer() {
    const elementList = [];
    traverseExplorer(explorer, elementList, 0);
    return elementList;
  }

  return (
    <main className='container'>{parseExplorer()}</main>
  );
}
.container {
    width: 100%;
    border-radius: 4px;
    border: 1px solid #dadada;
    padding: 8px;
    height: 250px;
    overflow-y: scroll;
}

.itemContainer {
  padding: 8px;
  display: flex;
  justify-content: space-between;
  gap: 20px;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  gap: 10px;
}

.toolbar span {
  cursor: pointer;
}

.itemTitle {
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.arrow {
  color: black;
  opacity: 0.4;
  display: inline-block;
  margin-right: 5px;
  cursor: pointer;
}

.arrowExpanded {
  transform: rotate(90deg);
}