File Explorer - Machine Coding Question

Build a file explorer component using React with folder/file creation, navigation, and tree structure management

File Explorer - Machine Coding Question

Problem Statement

Create a file explorer component that allows users to:

  • Navigate through a hierarchical file/folder structure
  • Create new files and folders
  • Expand/collapse folders
  • Display files and folders with appropriate icons
  • Maintain the tree structure in state

🧠 Understanding the Problem

A file explorer is like the folder structure you see in your computer's file manager. Think of it as a tree where:

  • Folders are like branches that can contain other items
  • Files are like leaves that don't contain anything else
  • You can open/close folders to see what's inside
  • You can add new files or folders anywhere in the structure

🏗️ System Design & Architecture

Data Structure

We'll use a tree data structure where each node represents either a file or folder:

{
  id: "unique-id",
  name: "folder-or-file-name",
  isFolder: true/false,
  items: [] // only for folders
}

Component Architecture

App
└── FileExplorer
    └── Folder (recursive component)
        ├── Folder (for nested folders)
        └── File (for files)

Step-by-Step Implementation

Step 1: Project Setup

First, let's create our React project:

npx create-react-app file-explorer
cd file-explorer

Step 2: Understanding the Core Concepts

What is a Tree Data Structure?

Think of a tree like a family tree:

  • Root: The top-most folder (like a grandparent)
  • Nodes: Each folder or file (like family members)
  • Children: Items inside folders (like children)
  • Leaves: Files that don't have children

Why Use Recursion?

Since folders can contain other folders (which can contain more folders), we need a component that can call itself - this is called recursion. It's like a mirror reflecting another mirror!

Step 3: Creating the Data Structure

Let's create our sample data in src/data/folderData.js:

const explorer = {
  id: "1",
  name: "root",
  isFolder: true,
  items: [
    {
      id: "2",
      name: "public",
      isFolder: true,
      items: [
        {
          id: "3",
          name: "public nested 1",
          isFolder: true,
          items: [
            {
              id: "4",
              name: "index.html",
              isFolder: false,
              items: []
            },
            {
              id: "5",
              name: "hello.html",
              isFolder: false,
              items: []
            }
          ]
        },
        {
          id: "6",
          name: "public_nested_file",
          isFolder: false,
          items: []
        }
      ]
    },
    {
      id: "7",
      name: "src",
      isFolder: true,
      items: [
        {
          id: "8",
          name: "App.js",
          isFolder: false,
          items: []
        },
        {
          id: "9",
          name: "Index.js",
          isFolder: false,
          items: []
        },
        {
          id: "10",
          name: "styles.css",
          isFolder: false,
          items: []
        }
      ]
    },
    {
      id: "11",
      name: "package.json",
      isFolder: false,
      items: []
    }
  ]
};

export default explorer;

Step 4: Building the Recursive Folder Component

Create src/components/Folder.js:

import React, { useState } from "react";
import "../App.css";

const Folder = ({ explorer, handleInsertNode }) => {
  // State for showing input field when creating new items
  const [showInput, setShowInput] = useState({
    visible: false,
    isFolder: null,
  });
  
  // State for expanding/collapsing folders
  const [expand, setExpand] = useState(false);

  // Handle creating new folder or file
  const handleNewFolder = (e, isFolder) => {
    e.stopPropagation(); // Prevent folder from expanding when clicking button
    setExpand(true); // Auto-expand when adding new item
    setShowInput({
      visible: true,
      isFolder,
    });
  };

  // Handle pressing Enter to create new item
  const onAddFolder = (e) => {
    if (e.keyCode === 13 && e.target.value) {
      handleInsertNode(explorer.id, e.target.value, showInput.isFolder);
      setShowInput({ ...showInput, visible: false });
    }
  };

  // If it's a folder, render folder structure
  if (explorer.isFolder) {
    return (
      <div style={{ marginTop: 5, marginLeft: 10 }}>
        <div className="folder" onClick={() => setExpand(!expand)}>
          <span>📂 {explorer.name}</span>
          <div>
            <button onClick={(e) => handleNewFolder(e, true)}>Folder +</button>
            <button onClick={(e) => handleNewFolder(e, false)}>File +</button>
          </div>
        </div>
        
        {/* Show/hide folder contents */}
        <div style={{ display: expand ? "block" : "none", paddingLeft: 25 }}>
          {/* Input field for new items */}
          {showInput.visible && (
            <div className="inputContainer">
              <span>{showInput.isFolder ? "📂" : "🗄"}</span>
              <input
                autoFocus
                onKeyDown={onAddFolder}
                onBlur={() => setShowInput({ ...showInput, visible: false })}
                type="text"
                className="inputContainer__input"
              />
            </div>
          )}
          
          {/* Recursively render child items */}
          {explorer.items.map((item) => {
            return (
              <Folder 
                handleInsertNode={handleInsertNode} 
                explorer={item} 
                key={item.id} 
              />
            );
          })}
        </div>
      </div>
    );
  } else {
    // If it's a file, render file
    return <span className="file">🗄 {explorer.name}</span>;
  }
};

export default Folder;

Step 5: Creating the Tree Traversal Hook

Create src/hooks/use-traverse-tree.js:

const useTraverseTree = () => {
  // Function to insert a new node into the tree
  const insertNode = (tree, folderId, item, isFolder) => {
    // If we're at the root level
    if (tree.id === folderId) {
      const newItem = {
        id: new Date().getTime(),
        name: item,
        isFolder: isFolder,
        items: []
      };
      
      return {
        ...tree,
        items: [...tree.items, newItem]
      };
    }

    // Recursively search through the tree
    let latestNode = [];
    latestNode = tree.items.map((obj) => {
      return insertNode(obj, folderId, item, isFolder);
    });

    return { ...tree, items: latestNode };
  };

  return { insertNode };
};

export default useTraverseTree;

Step 6: Styling Our Component

Create src/App.css:

.app {
  background-color: #ebe1eb;
  min-height: 100vh;
  padding: 20px;
}

.folder {
  margin-top: 6px;
  background-color: rgb(194, 185, 185);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px;
  width: 300px;
  cursor: pointer;
  border-radius: 4px;
  transition: background-color 0.2s;
}

.folder:hover {
  background-color: rgb(180, 170, 170);
}

.inputContainer {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 5px 0;
}

.inputContainer > span {
  margin-top: 5px;
}

.inputContainer__input {
  margin: 6px 0 0px 0;
  padding: 5px;
  display: flex;
  border: 1px solid lightgray;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  border-radius: 3px;
}

.folder > span {
  margin: 0 5px;
}

.folder > div > button {
  font-size: 12px;
  background-color: white;
  border: 1px solid #ccc;
  padding: 4px 8px;
  margin-left: 5px;
  border-radius: 3px;
  cursor: pointer;
}

.folder > div > button:hover {
  background-color: #f0f0f0;
}

.file {
  margin-top: 5px;
  padding-left: 5px;
  display: flex;
  flex-direction: column;
  color: #333;
}

Step 7: Bringing It All Together

Update src/App.js:

import React, { useState } from 'react';
import explorer from './data/folderData';
import Folder from './components/Folder';
import './App.css';
import useTraverseTree from './hooks/use-traverse-tree';

const App = () => {
  const [explorerData, setExplorerData] = useState(explorer);
  const { insertNode } = useTraverseTree();

  const handleInsertNode = (folderId, item, isFolder) => {
    const finalTree = insertNode(explorerData, folderId, item, isFolder);
    setExplorerData(finalTree);
  };

  return (
    <div className='app'>
      <h1>📁 File Explorer</h1>
      <Folder handleInsertNode={handleInsertNode} explorer={explorerData} />
    </div>
  );
};

export default App;

Key Features Explained

1. Recursive Component Structure

The Folder component calls itself for nested folders. This is like a Russian doll - each doll contains smaller dolls inside!

2. State Management

  • expand: Controls if a folder is open or closed
  • showInput: Controls when to show the input field for new items
  • explorerData: Holds the entire tree structure

3. Tree Traversal

The insertNode function recursively searches through the tree to find the correct location to add new items.

4. Event Handling

  • Click on folder: Expand/collapse
  • Click on buttons: Show input field
  • Press Enter: Create new item
  • Click outside input: Cancel creation

🧩 Understanding the Code Flow

  1. Initial Render: App loads with the root folder structure
  2. User Clicks Folder: expand state toggles, showing/hiding contents
  3. User Clicks "+" Button: showInput becomes visible with input field
  4. User Types & Presses Enter: handleInsertNode is called
  5. Tree Update: insertNode finds the correct location and adds the new item
  6. Re-render: Component updates with the new structure

Running the Application

npm start

Your file explorer will be running at http://localhost:3000!

What You've Learned

  1. Tree Data Structures: How to represent hierarchical data
  2. Recursive Components: Components that can render themselves
  3. State Management: Managing complex nested state
  4. Event Handling: Managing user interactions
  5. CSS Styling: Creating a user-friendly interface

Possible Extensions

  • File Operations: Delete, rename, copy files
  • Drag & Drop: Move files between folders
  • File Preview: Show file contents
  • Search: Find files by name
  • Breadcrumbs: Show current path
  • Keyboard Navigation: Arrow keys to navigate

Tips for Interview

  1. Start Simple: Begin with just displaying the tree structure
  2. Add Features Incrementally: Add expand/collapse, then creation, then styling
  3. Explain Your Approach: Talk through your data structure choice
  4. Handle Edge Cases: What if the tree is empty? What if there are no permissions?
  5. Think About Performance: Large trees might need virtualization

🎉 Conclusion

Congratulations! You've built a functional file explorer that demonstrates:

  • Complex state management
  • Recursive component patterns
  • Tree data structure manipulation
  • User interaction handling

This is a great foundation for understanding how real file explorers work. The concepts you've learned here apply to many other scenarios like:

  • Menu systems
  • Organization charts
  • Comment threads
  • Category hierarchies

Keep exploring and building!