Building a VS Code File Tree in Next.js

In this comprehensive guide, we'll build our own VS Code-like file explorer from scratch using Next.js. We'll cover everything from data structures to recursive rendering, with plenty of examples along the way.
1. The Problem We're Solving
Imagine you have a flat list of files from an API:
[
{ "fileName": "backend/src/index.js", "id": "1", "sourceCode": "..." },
{ "fileName": "backend/package.json", "id": "2", "sourceCode": "..." },
{ "fileName": "frontend/src/App.jsx", "id": "3", "sourceCode": "..." },
{ "fileName": "frontend/src/components/Header.jsx", "id": "4", "sourceCode": "..." },
{ "fileName": "README.md", "id": "5", "sourceCode": "..." }
]
And you want to transform it into this beautiful, interactive tree:
📁 backend/
📁 src/
📄 index.js
📄 package.json
📁 frontend/
📁 src/
📄 App.jsx
📁 components/
📄 Header.jsx
📄 README.md
2. Understanding the Data
First, let's understand what we're working with. Our input data represents files with their full paths:
Input Data Structure
type SourceFile = {
id: string; // Unique identifier
fileName: string; // Full path: "backend/src/index.js"
sourceCode: string; // The actual file content
summary: string; // File description
projectId: string; // Project identifier
};
The Challenge
The main challenge is converting this flat structure into a hierarchical tree that mirrors the actual file system structure. Each fileName contains the full path, and we need to:
Split paths into individual segments
Create folder nodes for intermediate paths
Create file nodes for the final segments
Maintain proper parent-child relationships
3. Designing the Data Structures
The key to building a good file tree is designing the right data structures. We need to represent two types of nodes:
File Node
Represents an actual file (leaf node in our tree):
type FileNode = SourceFile & {
type: "file";
name: string; // Just the filename, e.g., "index.js"
};
Example:
{
type: "file",
name: "index.js",
id: "1",
fileName: "backend/src/index.js",
sourceCode: "import express from 'express'...",
summary: "Main server entry point",
projectId: "abc-123"
}
Folder Node
Represents a directory (branch node that can contain other nodes):
type FolderNode = {
type: "folder";
name: string; // Just the folder name, e.g., "src"
children: FileTree; // Recursive reference!
};
Example:
{
type: "folder",
name: "backend",
children: {
"src": { type: "folder", name: "src", children: {...} },
"package.json": { type: "file", name: "package.json", ... }
}
}
File Tree
The overall structure that holds our tree:
type FileTree = {
[key: string]: FileNode | FolderNode;
};
Why this design works:
Union Types: Each tree node can be either a file or folder
Type Discrimination: The
typefield lets us distinguish between themRecursion:
FolderNode.childrenis itself aFileTreeFlexible Keys: Object keys represent file/folder names for easy lookup
Complete Type System
// Base input data
type SourceFile = {
id: string;
fileName: string;
sourceCode: string;
summary: string;
projectId: string;
};
// Tree node types
type FileNode = SourceFile & {
type: "file";
name: string;
};
type FolderNode = {
type: "folder";
name: string;
children: FileTree;
};
// The main tree structure
type FileTree = {
[key: string]: FileNode | FolderNode;
};
4. Building the Tree Algorithm
Now for the fun part - converting our flat list into a hierarchical tree!
The Algorithm Intuition
Think of it like organizing physical files:
You have a stack of documents with full addresses
For each document, you follow its address step-by-step
If a folder doesn't exist, you create it
You place the document in the deepest folder
Step-by-Step Implementation
const buildFileTree = (files: SourceFile[]): FileTree => {
const root: FileTree = {};
files.forEach((file) => {
let currentLevel = root;
// Split the path: "backend/src/index.js" → ["backend", "src", "index.js"]
const pathParts = file.fileName.split("/");
pathParts.forEach((part, index) => {
const isFile = index === pathParts.length - 1;
if (isFile) {
currentLevel[part] = {
type: "file",
name: part,
...file,
};
} else {
if (!currentLevel[part]) {
currentLevel[part] = {
type: "folder",
name: part,
children: {},
};
}
currentLevel = (currentLevel[part] as FolderNode).children;
}
});
});
return root;
};
Let's see what happens with multiple files:
const files = [
{ fileName: "backend/src/index.js", id: "1", /* ... */ },
{ fileName: "backend/package.json", id: "2", /* ... */ },
{ fileName: "frontend/src/App.jsx", id: "3", /* ... */ },
{ fileName: "README.md", id: "4", /* ... */ }
];
const tree = buildFileTree(files);
// Result:
{
"backend": {
type: "folder",
name: "backend",
children: {
"src": {
type: "folder",
name: "src",
children: {
"index.js": { type: "file", name: "index.js", id: "1" }
}
},
"package.json": { type: "file", name: "package.json", id: "2" }
}
},
"frontend": {
type: "folder",
name: "frontend",
children: {
"src": {
type: "folder",
name: "src",
children: {
"App.jsx": { type: "file", name: "App.jsx", id: "3" }
}
}
}
},
"README.md": { type: "file", name: "README.md", id: "4" }
}
5. Rendering with Recursion
Now that we have our tree structure, we need to render it. This is where recursion shines!
The Recursive Component
interface FileTreeProps {
tree: FileTree;
onFileSelect: (file: FileNode) => void;
selectedFileId: string | null;
level?: number; // For indentation
}
const FileTreeView: React.FC<FileTreeProps> = ({
tree,
onFileSelect,
selectedFileId,
level = 0
}) => {
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({});
const toggleFolder = (name: string) => {
setOpenFolders(prev => ({
...prev,
[name]: !prev[name]
}));
};
// Sort entries: folders first, then files, alphabetically within each group
const sortedEntries = Object.values(tree).sort((a, b) => {
if (a.type === 'folder' && b.type === 'file') return -1;
if (a.type === 'file' && b.type === 'folder') return 1;
return a.name.localeCompare(b.name);
});
return (
<div>
{sortedEntries.map(node => {
if (node.type === 'folder') {
const isFolderOpen = openFolders[node.name];
return (
<div key={node.name} style={{ marginLeft: `${level * 16}px` }}>
<div
className="flex items-center p-1 cursor-pointer rounded-md hover:bg-gray-700"
onClick={() => toggleFolder(node.name)}
>
{isFolderOpen ? <ChevronDown /> : <ChevronRight />}
<FolderIcon />
<span className="ml-1 font-semibold">{node.name}</span>
</div>
{/* recursive call */}
{isFolderOpen && (
<FileTreeView
tree={node.children}
onFileSelect={onFileSelect}
selectedFileId={selectedFileId}
level={level + 1} // Increase indentation
/>
)}
</div>
);
} else {
const isSelected = selectedFileId === node.id;
return (
<div
key={node.id}
style={{ marginLeft: `${level * 16}px` }}
className={`flex items-center p-1 cursor-pointer rounded-md hover:bg-gray-700 ${
isSelected ? 'bg-blue-600/50' : ''
}`}
onClick={() => onFileSelect(node)}
>
<FileIcon fileName={node.name} />
<span className="ml-2">{node.name}</span>
</div>
);
}
})}
</div>
);
};
How Recursion Works Here
The magic happens in this part:
{isFolderOpen && (
<FileTreeView
tree={node.children} // Pass the folder's children
onFileSelect={onFileSelect}
selectedFileId={selectedFileId}
level={level + 1} // Increase nesting level
/>
)}
The recursive flow:
Initial call renders root level folders and files
When a folder is clicked,
toggleFoldersetsopenFolders[folderName] = trueComponent re-renders,
isFolderOpenbecomestrueRecursive call renders the folder's contents with
level + 1If nested folders exist, the process repeats at deeper levels
Visual Example
For this tree structure:
📁 backend/ (level 0)
📁 src/ (level 1)
📄 index.js (level 2)
📄 package.json (level 1)
The rendering calls would be:
// Initial call (level 0)
<FileTreeView tree={rootTree} level={0} />
// When "backend" is opened (level 1)
<FileTreeView tree={backend.children} level={1} />
// When "src" is opened (level 2)
<FileTreeView tree={src.children} level={2} />
Adding Interactivity with Folder State Management
Let's add the interactive features that make our file tree feel alive:
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({});
const toggleFolder = (name: string) => {
setOpenFolders(prev => ({
...prev,
[name]: !prev[name] // Toggle: undefined -> true, true -> false, false -> true
}));
};
6. Performance Issues with Large Trees
// ❌ INEFFICIENT - Recreates sorted array on every render
const sortedEntries = Object.values(tree).sort((a, b) => {
// Expensive sorting on every render
});
// ✅ OPTIMIZED - Memoize the sorted result
const sortedEntries = useMemo(() => {
return Object.values(tree).sort((a, b) => {
if (a.type === 'folder' && b.type === 'file') return -1;
if (a.type === 'file' && b.type === 'folder') return 1;
return a.name.localeCompare(b.name);
});
}, [tree]);
7. Complete Working Example
Here's a complete, working implementation you can copy and use:
Types (types/fileTree.ts)
export type SourceFile = {
id: string;
fileName: string;
sourceCode: string;
summary: string;
projectId: string;
};
export type FileNode = SourceFile & {
type: "file";
name: string;
};
export type FolderNode = {
type: "folder";
name: string;
children: FileTree;
};
export type FileTree = {
[key: string]: FileNode | FolderNode;
};
Tree Builder (utils/buildFileTree.ts)
import { SourceFile, FileTree, FolderNode } from '../types/fileTree';
export const buildFileTree = (files: SourceFile[]): FileTree => {
const root: FileTree = {};
files.forEach(file => {
let currentLevel = root;
const pathParts = file.fileName.split('/');
pathParts.forEach((part, index) => {
const isFile = index === pathParts.length - 1;
if (isFile) {
currentLevel[part] = {
type: 'file',
name: part,
...file,
};
} else {
if (!currentLevel[part]) {
currentLevel[part] = {
type: 'folder',
name: part,
children: {},
};
}
currentLevel = (currentLevel[part] as FolderNode).children;
}
});
});
return root;
};
File Tree Component (components/FileTreeView.tsx)
import React, { useState, useMemo } from 'react';
import { FileTree, FileNode, FolderNode } from '../types/fileTree';
import { ChevronRight, ChevronDown, Folder, FolderOpen } from 'lucide-react';
interface FileTreeProps {
tree: FileTree;
onFileSelect: (file: FileNode) => void;
selectedFileId: string | null;
level?: number;
}
const FileIcon = ({ fileName }: { fileName: string }) => {
const extension = fileName.split('.').pop()?.toLowerCase();
const iconClass = "w-4 h-4 mr-2";
switch (extension) {
case 'ts':
case 'tsx':
return <div className={`${iconClass} bg-yellow-400 rounded text-xs text-black font-bold flex items-center justify-center`}>JS</div>;
default:
return <div className={`${iconClass} bg-gray-400 rounded text-xs text-black font-bold flex items-center justify-center`}>📄</div>;
}
};
export const FileTreeView: React.FC<FileTreeProps> = ({
tree,
onFileSelect,
selectedFileId,
level = 0
}) => {
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({});
const toggleFolder = (name: string) => {
setOpenFolders(prev => ({
...prev,
[name]: !prev[name]
}));
};
const sortedEntries = useMemo(() => {
return Object.values(tree).sort((a, b) => {
if (a.type === 'folder' && b.type === 'file') return -1;
if (a.type === 'file' && b.type === 'folder') return 1;
return a.name.localeCompare(b.name);
});
}, [tree]);
return (
<div className="select-none">
{sortedEntries.map(node => {
if (node.type === 'folder') {
const isFolderOpen = openFolders[node.name];
return (
<div key={node.name}>
<div
style={{ paddingLeft: `${level * 12}px` }}
className="flex items-center py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 rounded text-sm"
onClick={() => toggleFolder(node.name)}
>
<div className="w-4 h-4 mr-1 flex items-center justify-center">
{isFolderOpen ? (
<ChevronDown className="w-3 h-3" />
) : (
<ChevronRight className="w-3 h-3" />
)}
</div>
{isFolderOpen ? (
<FolderOpen className="w-4 h-4 mr-2 text-blue-500" />
) : (
<Folder className="w-4 h-4 mr-2 text-blue-600" />
)}
<span className="font-medium">{node.name}</span>
</div>
{isFolderOpen && (
<FileTreeView
tree={node.children}
onFileSelect={onFileSelect}
selectedFileId={selectedFileId}
level={level + 1}
/>
)}
</div>
);
} else {
const isSelected = selectedFileId === node.id;
return (
<div
key={node.id}
style={{ paddingLeft: `${level * 12 + 16}px` }}
className={`flex items-center py-1 px-2 cursor-pointer rounded text-sm ${
isSelected
? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100'
: 'hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
onClick={() => onFileSelect(node)}
>
<FileIcon fileName={node.name} />
<span>{node.name}</span>
</div>
);
}
})}
</div>
);
};
Usage Example (App.tsx)
import React, { useState, useMemo } from 'react';
import { FileTreeView } from './components/FileTreeView';
import { buildFileTree } from './utils/buildFileTree';
import { SourceFile, FileNode } from './types/fileTree';
const App = () => {
const [selectedFile, setSelectedFile] = useState<FileNode | null>(null);
// Mock data - replace with your API call
const mockFiles: SourceFile[] = [
{
id: "1",
fileName: "backend/src/index.js",
sourceCode: "import express from 'express';\n// Server code...",
summary: "Main server entry point",
projectId: "proj-1"
},
{
id: "2",
fileName: "backend/package.json",
sourceCode: '{\n "name": "backend",\n "version": "1.0.0"\n}',
summary: "Backend dependencies",
projectId: "proj-1"
},
{
id: "3",
fileName: "frontend/src/App.jsx",
sourceCode: "import React from 'react';\n\nfunction App() {\n return <h1>Hello World</h1>;\n}",
summary: "Main React component",
projectId: "proj-1"
},
{
id: "4",
fileName: "frontend/src/components/Header.jsx",
sourceCode: "import React from 'react';\n\nexport const Header = () => {\n return <header>My App</header>;\n};",
summary: "Header component",
projectId: "proj-1"
},
{
id: "5",
fileName: "README.md",
sourceCode: "# My Project\n\nThis is a sample project...",
summary: "Project documentation",
projectId: "proj-1"
}
];
const fileTree = useMemo(() => buildFileTree(mockFiles), []);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="flex">
{/* File Tree Sidebar */}
<div className="w-80 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 h-screen overflow-auto">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Project Files
</h2>
</div>
<div className="p-2">
<FileTreeView
tree={fileTree}
onFileSelect={setSelectedFile}
selectedFileId={selectedFile?.id || null}
/>
</div>
</div>
{/* Content Area */}
<div className="flex-1 p-6">
{selectedFile ? (
<div>
<div className="mb-4">
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">
{selectedFile.fileName}
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{selectedFile.summary}
</p>
</div>
{/* Code Editor */}
<div className="bg-gray-900 rounded-lg p-4 overflow-auto">
<pre className="text-green-400 text-sm">
<code>{selectedFile.sourceCode}</code>
</pre>
</div>
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Welcome to the File Explorer
</h3>
<p className="text-gray-500 dark:text-gray-400">
Select a file from the sidebar to view its contents
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default App;
8. Conclusion
Building a VS Code-like file tree involves several key concepts:
Smart Data Structures: Using recursive types to mirror the hierarchical nature of file systems
Efficient Algorithms: Converting flat file lists to tree structures with proper state management
Recursive Rendering: Using React's component model to handle arbitrary nesting levels
Interactive State: Managing folder open/close states and file selection
Performance: Memoizing expensive calculations and considering virtualization for large datasets
The beauty of this approach is its scalability - whether you have 10 files or 10,000 files, the same patterns and algorithms work efficiently.
What's Next?
From here, you could extend this file tree with:
Real-time file watching and updates
Multi-select functionality
Custom file type handling
Integration with code editors
File operations (create, rename, delete)
Git status indicators
Breadcrumb navigation
The patterns you've learned here form the foundation for building any hierarchical data visualization in React. Whether it's file trees, organization charts, or nested comment systems, these same principles apply.
Happy coding!
Found this helpful? Follow me for more in-depth React tutorials and don't forget to share this with your developer friends!




