Recursive component in React
Building a Recursive File Tree Component in React
In this blog, I will walk you through creating a recursive file tree component in React. This component can be particularly useful for applications that need to display hierarchical data structures, such as file directories. We will create two main components: Source
and File
.
Components Overview
Source: The main component responsible for managing the root nodes.
File: A recursive component representing individual file/directory nodes, displaying them in a hierarchical structure.
Both components will listen to Electron API events to fetch data and update the UI.
Let's Dive In
Here is the complete code for the Source
and File
components:
import React, { useEffect, useState, useCallback } from 'react';
import { MdExpandLess, MdExpandMore } from 'react-icons/md';
import log from 'electron-log/renderer';
// Interface representing a file/directory node structure
interface FileNode {
id: string;
name: string;
children?: FileNode[];
isDirectory: boolean;
}
/**
* File Component
* ---------------
* Recursive component to represent file/directory nodes in a hierarchical tree.
* - Listens to events to load subdirectory contents dynamically.
* - Handles toggle visibility of subdirectories.
*/
const File: React.FC<FileNode> = ({ id, name, children, isDirectory }: FileNode) => {
const [showChildren, setShowChildren] = useState(false); // Toggle subdirectory visibility
const [childNodes, setChildNodes] = useState(children || []); // Store subdirectory nodes
// Handle node click event: toggle subdirectory visibility or fetch contents
const handleClick = useCallback(() => {
setShowChildren((prev) => !prev);
log.debug('handleClick for', id);
if (isDirectory) {
// Fetch contents only if it is a directory and not already loaded
if (!children || childNodes?.length === 0) {
log.debug('sending get-directory-contents for', id);
window.api.send('get-directory-contents', id);
}
} else {
// Fetch file contents
log.debug('sending get-file-contents for', id);
window.api.send('get-file-contents', id);
}
}, [id, childNodes?.length, isDirectory, children]);
// Listen for directory contents and update nodes if relevant
useEffect(() => {
const updateChildNodes = (contents: { parentId: string; contents: FileNode[] }) => {
if (id === contents.parentId) {
setChildNodes(contents.contents);
}
};
window.api.receive('directory-contents', updateChildNodes);
// Cleanup listener on component unmount
return () => {
window.api.removeAllListeners('directory-contents');
};
}, [id]);
// Render the file node with a recursive subdirectory expansion
return (
<div>
<div onClick={handleClick} className="cursor-pointer flex flex-row">
{/* Toggle expand/collapse icon for directories */}
{isDirectory ? (showChildren ? <MdExpandLess /> : <MdExpandMore />) : null}
<span>
<h4 className={`${showChildren ? 'font-bold' : 'font-normal'}`}>{name}</h4>
</span>
</div>
{showChildren && (
<div className="ml-6 mr-6 border-l pl-4">
{childNodes?.map((node) => (
<File key={node.id} {...node} />
))}
</div>
)}
</div>
);
};
/**
* Source Component
* -----------------
* Main component that manages and displays the root nodes for the file tree structure.
* - Receives and processes the directory contents event from the main process.
*/
const Source = (): JSX.Element => {
const [nodes, setNodes] = useState<FileNode[]>([]);
// Listen for root directory contents and update state
useEffect(() => {
const updateSourceNodes = (contents: FileNode[]) => {
console.log('Received source-directory-contents', contents);
setNodes(contents);
};
window.api.receive('source-directory-contents', updateSourceNodes);
// Cleanup listener on component unmount
return () => {
window.api.removeAllListeners('source-directory-contents');
};
}, []);
// Render the root nodes with the File component
return (
<div className="inline-block bg-gray-700 flex-1 font-normal text-white text-[16px] leading-[24px]">
{nodes.map((node) => (
<div className="ml-4 mr-6" key={node.id}>
<File id={node.id} name={node.name} isDirectory={node.isDirectory} />
</div>
))}
</div>
);
};
export default Source;
How It Works
File Component
The File
component is recursive, meaning it can call itself to display nested directories. Here's a breakdown of its key parts:
State Management: We use the
useState
hook to manage the visibility of child nodes and store the subdirectory nodes.Event Handling: The
handleClick
function toggles the visibility of child nodes. If the node is a directory and its contents are not already loaded, it sends a request to load them.Side Effects: We use the
useEffect
hook to listen for directory contents updates. When the component mounts, it sets up a listener for the 'directory-contents' event and cleans it up when the component unmounts.
Source Component
The Source
component manages and displays the root nodes for the file tree structure. Here's a breakdown of its key parts:
State Management: We use the
useState
hook to store the root nodes.Event Handling: We use the
useEffect
hook to listen for the 'source-directory-contents' event and update the root nodes accordingly.
Handling Event Listeners and Memory Leaks
While developing and testing, you might encounter issues related to memory leaks or too many event listeners being added. Here are some tips to handle these issues:
Remove Event Listeners: Always clean up your event listeners in the
useEffect
cleanup function to avoid memory leaks. This is crucial, especially in recursive components where multiple instances might be created and destroyed.jsxCopy codeExplainuseEffect(() => { const updateChildNodes = (contents: { parentId: string; contents: FileNode[] }) => { if (id === contents.parentId) { setChildNodes(contents.contents); } }; window.api.receive('directory-contents', updateChildNodes); // Cleanup listener on component unmount return () => { window.api.removeAllListeners('directory-contents'); }; }, [id]);
Check for Event Listener Count: If you find that the number of event listeners exceeds a certain limit (e.g., more than 18), it might indicate a memory leak. Make sure that each listener is added and removed correctly.
Debugging Memory Leaks: Use tools like Chrome DevTools to monitor memory usage and identify leaks. Check for detached DOM nodes and uncollected garbage that might indicate a memory leak.
In DevTools, go to the "Memory" tab, take a heap snapshot, and analyze the memory allocation.
Look for any large arrays or objects that shouldn't be there and trace back to find the cause.
Conclusion
By combining React's state management with recursive components, we can create a dynamic and interactive file tree structure. This approach is not limited to file systems and can be adapted to any hierarchical data structure.
Feel free to use and modify this code to fit your needs. Happy coding!
Additional Tips
Styling: You can customize the styling using CSS or libraries like TailwindCSS.
Performance: For large directories, consider optimizing performance by lazy-loading nodes or using virtualization techniques.
I hope this blog helps you understand how to build recursive components in React and handle potential memory leak issues. If you have any questions or feedback, feel free to leave a comment below.
Subscribe to my newsletter
Read articles from Shubham Bankar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by