Skip to main content

Command Palette

Search for a command to run...

Let's Explore this - File Explorer!!

Updated
14 min readView as Markdown
Let's Explore this - File Explorer!!

I was practicing for React coding rounds and encountered an interesting problem: building a File Explorer.

In this blog, we'll build it step by step, just like in an interview. Expect some refactoring along the way—it's part of the process! Optimization will happen naturally.

Before coding, I listed all the features and the sequence to follow. Here's the plan:

// TODO
// 1. Create a data structure with some dummy data
// 2. Display all file and folder names from the data
// 3. Build a UI that looks like a file explorer (tree structure)
//    - Add options to Add, Delete, and Rename
// 4. Implement open/close (expand/collapse) functionality for folders
// 5. Implement "Add File" and "Add Folder" functionality
//    - Handle input and placement correctly
// 6. Implement "Delete File" and "Delete Folder" functionality
// 7. Implement "Rename File" and "Rename Folder" functionality

Step 1 – Create a Vite Project

Why Vite? It’s faster than Create React App, so I prefer using it now.

Run the command below:

npm create vite@latest

It will ask for a project name and framework — choose React (JavaScript or TypeScript).

Step 2 – How to Design the Data

Before jumping in, always ask: Will the data come from an API, or do I need to design it myself?
In this case, there’s a high chance you’ll have to create it on your own.

So how do you decide what type of data to use, what keys to include, and how to structure it?

Here’s what helped me — I simply looked at the folder structure of my own project for reference.

So first, create a data.json file in your project root and just look at your folders.
What do you see?

now, what do you see?

  1. Some files

  2. Some folders

  3. Each has a unique name

  4. Folders can contain files/folders — a clear parent–child relationship

Using these observations, we'll create our JSON data structure.

Here's a small example for you to see:

[
  {
    "name": "node_modules",
    "isFolder": true,
    "id": 1,
    "children": [
      {
        "name": "react",
        "isFolder": true,
        "id": 2,
        "children": []
      },
      {
        "name": "react-dom",
        "id": 3,
        "isFolder": false
      }
    ]
  },
  {
    "name": "src",
    "isFolder": true,
    "id": 4,
    "children": [
      {
        "name": "App.jsx",
        "id": 5,
        "isFolder": false
      },
      {
        "name": "data.json",
        "id": 6,
        "isFolder": false
      },
      {
        "name": "components",
        "id": 7,
        "isFolder": true,
        "children": []
      },
    ]
  },
  {
    "name": "index.html",
    "id": 10,
    "isFolder": false
  },
  {
    "name": "pacakage.json",
    "id": 11,
    "isFolder": false
  }
]

Now that our first and very important step is complete, let's move on to the UI.

Step 3 - Display all file and folder names from the data

we will do it two phase

  • first we will only render the main parents

  • second we will see what to do with child

import data from "./data.json";

function App() {
  return (
    <>
      File explorer
      {/* printing just the parent  */}
      {data?.map((ele) => (
        <>
          <p>{ele?.name}</p>
        </>
      ))}
    </>
  );
}

export default App;

Now for the second phase — printing the children.

The challenge is the unknown levels of nesting, which can be infinite.

This is where DSA helps, especially Recursion.

How recursion works here

  • Process each object (file or folder).

  • If it’s a file, print it.

  • If it’s a folder, print its name and call the function again for its children.

  • Repeat until there are no children left.

Recursion allows one function to handle all nesting levels automatically.

import { useState } from "react";
import File from "./components/File";
import data from "./data.json";

function App() {
  // State to manage the folder structure data
  const [folderData, setFolderData] = useState(data);

  return (
    <div style={{ width: "max-content" }}>
      <h1>File Explorer</h1>

      {/* 
        Loop through the root-level folder data and render File components 
        for each item (file or folder)
      */}
      {folderData?.map((ele) => (
        <File
          key={ele.name}
          data={ele}
          folderData={folderData}
          setFolderData={setFolderData}
        />
      ))}
    </div>
  );
}

export default App;

File Component

The File component represents a single file or folder in our explorer.

const File = ({ data, folderData, setFolderData }) => {
  return (
    <div>
      {/* 
        Main container for the file/folder item 
        Uses flexbox to align content properly
      */}
      <div style={{ display: "flex", justifyContent: "space-between" }}>
        <div
          style={{
            display: "flex",
            alignItems: "center",
            // Cursor changes to pointer for folders to show interactivity
            cursor: data?.isFolder ? "pointer" : "default",
            gap: "6px",
          }}
        >
          {/* Display the name of the file or folder */}
          <span>{data?.name}</span>
        </div>
      </div>

      {/* 
        Indented container for children elements (sub-folders or files)
        This creates the nested tree structure visually
      */}
      <div style={{ paddingLeft: "20px" }}>
        {/* 
          Recursively render File components for each child item
          This enables the nested folder structure
        */}
        {data?.children?.map((ch) => (
          <File
            key={ch.name}
            data={ch}
            folderData={folderData}
            setFolderData={setFolderData}
          />
        ))}
      </div>
    </div>
  );
};

export default File;

What’s happening here

  • Renders the current file or folder name.

  • Checks: “Do I have children?”

  • If NO children → stop recursion.

  • If HAS children → recursive call:

    • Loops through each child.

    • Creates a new File component for each one.

    • Passes the child data down.

    • Adds indentation for visual hierarchy.

That’s it — recursion in action, and our file explorer now handles infinite nesting automatically.

Step 3 - Build a UI like a file explorer

  • Give every file/folder options to Add, Delete, and Rename

  • Use basic CSS styling — nothing logic-heavy yet

const File = ({ data, folderData, setFolderData }) => {
  return (
    <div>
      {/* 
        Main row for each file/folder.
        We use flexbox to separate the left (name) and right (actions).
      */}
      <div style={{ display: "flex", justifyContent: "space-between" }}>
        {/* 
          LEFT SIDE → Folder/File name with toggle area.
          If it’s a folder, we show pointer cursor (indicating it can be expanded later).
        */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            cursor: data?.isFolder ? "pointer" : "default",
            gap: "6px",
          }}
        >
          {/* Display file/folder name */}
          <span>{data?.name}</span>
        </div>

        {/* 
          RIGHT SIDE → Action icons.
          - All items (file/folder) have Delete and Rename options.
          - Only folders have Add File and Add Folder options.
        */}
        <div style={{ display: "flex", gap: "5px" }}>
          {data?.isFolder && (
            <>
              {/* Add File option (only for folders) */}
              <p style={{ margin: 0, cursor: "pointer" }} title="Add file">
                🗃️
              </p>

              {/* Add Folder option (only for folders) */}
              <p style={{ margin: 0, cursor: "pointer" }} title="Add folder">
                📁
              </p>
            </>
          )}

          {/* Delete option (for both files and folders) */}
          <p style={{ margin: 0, cursor: "pointer" }} title="Delete">
            🗑️
          </p>

          {/* Rename option (for both files and folders) */}
          <p style={{ margin: 0, cursor: "pointer" }} title="Rename">
            🖊️
          </p>
        </div>
      </div>

      {/* 
        CHILDREN SECTION
        - Add indentation to show folder hierarchy visually.
        - Recursively render File components for all children.
      */}
      <div style={{ paddingLeft: "20px" }}>
        {data?.children?.map((ch) => (
          <File
            key={ch.name}
            data={ch}
            folderData={folderData}
            setFolderData={setFolderData}
          />
        ))}
      </div>
    </div>
  );
};

export default File;

Here i have added icons depending upon file or folder -

  • Each file/folder can now be renamed or deleted.

  • Only folders have the option to add new files or folders inside them.

Step 4 - Implement open/close (expand/collapse) functionality for folders

Here, if the current data is of folder type, we’ll toggle between expanded and collapsed states on click.
We’ll render its children only when it’s expanded.

import { useState } from "react";

const File = ({ data, folderData, setFolderData }) => {
  // Local state to manage expand/collapse for this folder
  const [isExpanded, setIsExpanded] = useState(true);

  return (
    <div>
      {/* Main row for file/folder */}
      <div style={{ display: "flex", justifyContent: "space-between" }}>

        {/* Left side: Folder/File name + expand toggle */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            cursor: data?.isFolder ? "pointer" : "default",
            gap: "6px",
          }}
          onClick={() => {
            // If it's a folder, toggle expand/collapse
            if (data?.isFolder) setIsExpanded((prev) => !prev);
          }}
        >
          {/* Arrow for expand/collapse (visible only for folders) */}
          {data?.isFolder && (
            <span>{isExpanded ? "▼" : "▶"}</span>
          )}

          {/* File or folder name */}
          <span>{data?.name}</span>
        </div>

        {/* rest code remains same*/}

      </div>

      {/* Recursive rendering of children - only when expanded */}
      {isExpanded && (
        <div style={{ paddingLeft: "20px" }}>
          {data?.children?.map((ch) => (
            <File
              key={ch.name}
              data={ch}
              folderData={folderData}
              setFolderData={setFolderData}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export default File;

The logic-

  • isExpanded → A state that keeps track of whether a folder is open or closed.

  • When you click on a folder name, it toggles between true/false.

  • Children are rendered only if isExpanded is true (that’s our condition).

  • The arrow symbol ( or ) visually indicates whether the folder is expanded or collapsed.

Now comes the big guns ADDING and DELETEING!!

Step 5 - Implement "Add File" and "Add Folder" functionality

Here, we'll explore each step in detail:

  • Step 1: Add isAdding and isFile states, and connect them to icons.

  • Step 2: Add newFileFolderName and show <input> when isAdding is true.

  • Step 3: Use Enter/Escape/Blur to confirm or cancel input.

  • Step 4: Create handleAddingFileFolder to update the tree, and call it on Enter/Blur.

  1. Track what we are adding (file or folder):
    Action: Add isAdding (to show input) and isFile (true for file, false for folder) states. Connect these states to the add icons.

     import { useState } from "react";
    
     const File = ({ data, folderData, setFolderData }) => {
       const [isAdding, setIsAdding] = useState(false); // show/hide input
       const [isFile, setIsFile] = useState(null); // true => file, false => folder
    
       return (
         <div>
           <div style={{ display: "flex", justifyContent: "space-between" }}>
             <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
               <span>{data?.name}</span>
             </div>
    
             <div style={{ display: "flex", gap: "5px" }}>
               {data?.isFolder && (
                 <>
                   <p
                     style={{ margin: 0, cursor: "pointer" }}
                     title="Add file"
                     onClick={() => {
                       setIsAdding(true);
                       setIsFile(true);
                     }}
                   >
                     🗃️
                   </p>
                   <p
                     style={{ margin: 0, cursor: "pointer" }}
                     title="Add folder"
                     onClick={() => {
                       setIsAdding(true);
                       setIsFile(false);
                     }}
                   >
                     📁
                   </p>
                 </>
               )}
               <p style={{ margin: 0, cursor: "pointer" }} title="Delete">🗑️</p>
               <p style={{ margin: 0, cursor: "pointer" }} title="Rename">🖊️</p>
             </div>
           </div>
    
           {/* children recursion placeholder */}
           <div style={{ paddingLeft: "20px" }}>
             {data?.children?.map((ch) => (
               <File key={ch.name} data={ch} folderData={folderData} setFolderData={setFolderData} />
             ))}
           </div>
         </div>
       );
     };
    
     export default File;
    
  2. Render the input when adding (positioned under the folder where user clicked)

    What to do: add newFileFolderName state and render an <input> when isAdding is true. No add logic yet — just UI.

     import { useState } from "react";
    
     const File = ({ data, folderData, setFolderData }) => {
       const [isAdding, setIsAdding] = useState(false);
       const [isFile, setIsFile] = useState(null);
       const [newFileFolderName, setNewFileFolderName] = useState("");
    
       return (
         <div>
           <div style={{ display: "flex", justifyContent: "space-between" }}>
             <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
               <span>{data?.name}</span>
             </div>
    
             <div style={{ display: "flex", gap: "5px" }}>
               {data?.isFolder && (
                 <>
                   <p style={{ margin: 0, cursor: "pointer" }} title="Add file" onClick={() => { setIsAdding(true); setIsFile(true); }}>🗃️</p>
                   <p style={{ margin: 0, cursor: "pointer" }} title="Add folder" onClick={() => { setIsAdding(true); setIsFile(false); }}>📁</p>
                 </>
               )}
               <p style={{ margin: 0, cursor: "pointer" }} title="Delete">🗑️</p>
               <p style={{ margin: 0, cursor: "pointer" }} title="Rename">🖊️</p>
             </div>
           </div>
    
           {/* add input (UI only) */}
           {isAdding && (
             <input
               autoFocus
               value={newFileFolderName}
               onChange={(e) => setNewFileFolderName(e.target.value)}
               style={{ marginLeft: "20px" }}
             />
           )}
    
           <div style={{ paddingLeft: "20px" }}>
             {data?.children?.map((ch) => (
               <File key={ch.name} data={ch} folderData={folderData} setFolderData={setFolderData} />
             ))}
           </div>
         </div>
       );
     };
    
     export default File;
    
  3. Wire input behavior: Enter (confirm), Escape (cancel), Blur (confirm)

    What to do: add onKeyDown and onBlur to the input. For now these will toggle UI; the real insertion function will be added next. Make sure Enter prevents default.

       const cancelAdd = () => {
         setIsAdding(false);
         setNewFileFolderName("");
       };
            // in ui 
    
           {isAdding && (
             <input
               autoFocus
               value={newFileFolderName}
               onChange={(e) => setNewFileFolderName(e.target.value)}
               onKeyDown={(e) => {
                 if (e.key === "Enter") {
                   e.preventDefault();
                   // placeholder: actual add function will be called here in next step
                   setIsAdding(false);
                   setNewFileFolderName("");
                 }
                 if (e.key === "Escape") cancelAdd();
               }}
               onBlur={() => {
                 // placeholder: call add function here later
                 if (newFileFolderName.trim()) {
                   setIsAdding(false);
                   setNewFileFolderName("");
                 } else cancelAdd();
               }}
               style={{ marginLeft: "20px" }}
             />
           )}
    
  4. Main logic: build handleAddingFileFolder and insert immutably

    now we will write a function that, given the clicked node name and isFolder, returns updated tree with new node appended to that node’s children. We do this immutably and recursively.

    4.a) Helper idea

    • Create a recursive updateItems(list) that returns a new list where the item with matching name has the new child appended.

    • If a node has children, call updateItems on them.

4.b) Implement the function and call setFolderData(prev => updateItems(prev)).

4.c) Wire it to Enter/Blur: call handleAddingFileFolder(data.name, !isFile).


      // main insertion logic (recursive, immutable)
      const handleAddingFileFolder = (id, isFolder) => {
        if (!newFileFolderName.trim()) return;

        const updateItems = (list) => {
          return list?.map((a) => {
            // match by id instead of name
            if (a.id === id) {
              const newItem = {
                id: Date.now(),
                name: newFileFolderName,
                isFolder,
                children: isFolder ? [] : undefined,
              };
              return { ...a, children: [...(a.children || []), newItem] };
            }

            // recurse through children if any
            if (a?.children) {
              return { ...a, children: updateItems(a.children) };
            }

            return a;
          });
        };

        setFolderData((prev) => updateItems(prev));
        setIsAdding(false);
        setNewFileFolderName("");
      };

How it finds the right spot:

  • It goes through every folder

  • When it finds a folder with matching id→ adds the new item there

  • If folder has sub-folders → searches inside them too

  • Returns everything else unchanged

Wow, that was intense! Trust me, things are much easier now!

Step 5 - Implement "Delete File" and "Delete Folder" functionality

It's pretty simple and similar to adding functionality. We will focus on the main delete function here.

const handleDelete = (idToDelete) => {
  // Recursive function that searches through the entire tree
  const deleteItems = (list) => {
    // Step 1: Remove the item with matching ID from current level
    return list
      ?.filter((item) => item.id !== idToDelete)
      // Step 2: For all remaining items, check if they have children
      ?.map((item) => {
        // Step 3: If this item has children, we need to search inside them too
        if (item?.children) {
          return {
            ...item, // Keep all original properties of the item
            children: deleteItems(item.children) // Recursively search in children
          };
        }
        // Step 4: If no children, return the item as-is
        return item;
      });
  };

  // Update the global state with the new filtered tree
  setFolderData((prev) => deleteItems(prev));
};
  1. Start searching
  • Check each file/folder at the current level.
  1. Remove matching item
  • Remove the item if its ID matches the one to delete.

  • Keep the item if the ID doesn't match.

  1. Check for sub-folders
  • For remaining items with children, repeat the search process inside them.

  • If no children, leave the item as-is.

  1. Update everything
  • Save the new version without the deleted item.

  • Keep all other files/folders in place.

Now only one thing is left!!

Step 6 - Implement "Rename File" and "Rename Folder" functionality

  1. Add States + UI for Rename

What’s happening here

  • isRenaming: boolean → tells if we are currently renaming a file/folder

  • renameValue: stores the current rename input value (starts as the old name)

When user clicks ✏️ (rename icon):

  • We set isRenaming(true)

  • Show an input box in place of the file/folder name

  • Let user type new name

import { useState } from "react";

const File = ({ data, folderData, setFolderData }) => {
  const [isExpanded, setIsExpanded] = useState(true);

  // rename related states
  const [isRenaming, setIsRenaming] = useState(false); // tells if rename mode is active
  const [renameValue, setRenameValue] = useState(data.name); // stores current name or new input

  return (
    <div>
      {/* File/Folder Row */}
      <div style={{ display: "flex", justifyContent: "space-between" }}>
        {/* LEFT SIDE — File/Folder name */}
        <div
          style={{ display: "flex", alignItems: "center", gap: "6px", cursor: data?.isFolder ? "pointer" : "default" }}
          onClick={() => {
            if (data?.isFolder) setIsExpanded((prev) => !prev);
          }}
        >
          {data?.isFolder && <span>{isExpanded ? "▼" : "▶"}</span>}

          {/*  If rename mode is ON, show input else show normal name */}
          {isRenaming ? (
            <input
              autoFocus
              value={renameValue}
              onChange={(e) => setRenameValue(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") {
                  // later we’ll handle rename function here
                }
                if (e.key === "Escape") setIsRenaming(false);
              }}
              onBlur={() => setIsRenaming(false)}
            />
          ) : (
            <span>{data?.name}</span>
          )}
        </div>

        {/* RIGHT SIDE — buttons like rename, delete etc */}
        <div style={{ display: "flex", gap: "5px" }}>
          <p
            style={{ margin: 0, cursor: "pointer" }}
            title="Rename"
            onClick={() => {
              setRenameValue(data.name); // set current name
              setIsRenaming(true); // enter rename mode
            }}
          >
            🖊️
          </p>
        </div>
      </div>
    </div>
  );
};

export default File;
  1. Add Rename Logic Function
  // Step 2: Rename function
  const handleRename = (oldName, newName) => {
    if (!newName.trim()) return; // avoid empty names

    // Recursive function to update names in nested structure
    const updateItems = (list) => {
      return list?.map((a) => {
        if (a.name === oldName) {
          // if the name matches, update it
          return { ...a, name: newName };
        }
        if (a.children) {
          // if it has children, recursively update inside
          return { ...a, children: updateItems(a.children) };
        }
        return a;
      });
    };

    // set updated folder data
    setFolderData((prev) => updateItems(prev));
    setIsRenaming(false); // close rename mode
  };
  1. Start searching
  • Check the list of files/folders at the current level.

  • Examine each item.

  1. Find matching item
  • If an item's name matches the old name, update it.

  • If not, leave it unchanged.

  1. Check for sub-folders
  • For each item with children:

    • Repeat the search process inside.

    • Rename if found.

  • If no children, leave the item as-is.

  1. Update everything
  • Save the new version with the renamed item.

  • Keep all other files/folders in place.

We can add new features too - so go ahead pin me what more we can do!

Here is repo of my github link - https://github.com/vaishdwivedi1?tab=repositories

Deployed link - https://elegant-frangipane-07b2f1.netlify.app/

Do share your views!!