Tree view is one of the most popular components, you can see tree view everywhere from MacOS Finder or Window File Explorer or in any code editor like VSCode, Sublime Text, etc.

Although you can quickly find a Tree View package on Github but sometimes we might want to implement Tree View by yourself because there are some limitations of 3rd packages:

  • The package doesn’t allow you to change layout and design easily.
  • There are some different requirements in our tree view.
  • The package owner doesn’t maintain it anymore so it is left with many bugs.

Here is the demo of our tree view: http://davidtran.github.io/treeview/

And this is the source code: https://github.com/davidtran/simple-treeview

Let’s start!

Create project

We will use create-react-app to create our project. It’s my favorite tool to create any React project because it is very easy to use and fit all kind of projects. Now open your terminal and run the following commands:

npm install -g create-react-app 
create-react-app react-treeview

After create-react-app has created your project, you can run project by this command:

cd react-treeview
yarn start

The website will be opened in your browser. Any changes in source code will recompile your project and also reload website.

Now we need to install the following dependencies:

styled-components: I love this library because it has many advantages over SCSS and CSS Modules:

  • It utilizes ES6 template string and SCSS syntax so you can use Javascript in your styled component thus it is easy to change CSS properties by React props.
  • It is easier to share styled component than SCSS.
  • It is easier to recognize and eliminate any unused SCSS code in styled components.

lodash: this library contains some helpful javascript functions.

react-icons: this package contains most of the popular font icons like Font Awesome, Material or Ion Icons..

props-types: it is always necessary to enforce good coding style.

yarn add styled-components lodash react-icons prop-types --save

Start coding

Let’s open your favorite text editor and implement our Tree View

We will create 2 components: Tree.js and TreeNode.js. Put these components inside src/components.

Tree.js: this component is responsible to display and manipulate tree data. It also will render TreeNode.

TreeNode.js: this component is responsible to render node content.

We start with Tree.js first.

Tree.js

Open Tree.js and import these libraries:

import React, { Component } from 'react';
import values from 'lodash/values';

Besides React and Component, we also import values which comes handy when we want to convert object into array.

Now we defined some fake data for our tree view:

const data = {
  '/root': {
    path: '/root',
    type: 'folder',
    isRoot: true,
    children: ['/root/david', '/root/jslancer'],
  },
  '/root/david': {
    path: '/root/david',
    type: 'folder',
    children: ['/root/david/readme.md'],
  },
  '/root/david/readme.md': {
    path: '/root/david/readme.md',
    type: 'file',
    content: 'Thanks for reading me me. But there is nothing here.'
  },
  '/root/jslancer': {
    path: '/root/jslancer',
    type: 'folder',
    children: ['/root/jslancer/projects', '/root/jslancer/vblogs'],
  },
  '/root/jslancer/projects': {
    path: '/root/jslancer/projects',
    type: 'folder',
    children: ['/root/jslancer/projects/treeview'],
  },
  '/root/jslancer/projects/treeview': {
    path: '/root/jslancer/projects/treeview',
    type: 'folder',
    children: [],
  },
  '/root/jslancer/vblogs': {
    path: '/root/jslancer/vblogs',
    type: 'folder',
    children: [],
  },
};

As you can see, we store tree data in an object and we don’t have any nested object.

Every node is referenced by path property. This flat data structure makes it easy to quickly take data of any node and also quickly modify data.

It’s also easier to debug tree data because we don’t have to go to any nested object to read object content.

Here is the content of Tree class:

export default class Tree extends Component {

  state = {
    nodes: data,
  };

  getRootNodes = () => {
    const { nodes } = this.state;
    return values(nodes).filter(node => node.isRoot === true);
  }

  getChildNodes = (node) => {
    const { nodes } = this.state;
    if (!node.children) return [];
    return node.children.map(path => nodes[path]);
  }  

  onToggle = (node) => {
    const { nodes } = this.state;
    nodes[node.path].isOpen = !node.isOpen;
    this.setState({ nodes });
  }

  render() {
    const rootNodes = this.getRootNodes();
    return (
      <div>
        { rootNodes.map(node => (
          <TreeNode 
            node={node}
            getChildNodes={this.getChildNodes}          
          />
        ))}
      </div>
    )
  }
}

Tree.propTypes = {
  onSelect: PropTypes.func.isRequired,
};

There are 3 methods in this object:

  • getRootNodes: this method returns top-level node in tree data (isRoot === true)
  • getChildNodes: this method reads children property from a node and map with tree data to return an array of children objects.
  • In the render methods, we find root nodes and render them.

TreeNode.js

TreeNode is the component which renders content of item on our tree view. Let’s start by import these libraries:

import React from 'react';
import { FaFile, FaFolder, FaFolderOpen, FaChevronDown, FaChevronRight } from 'react-icons/fa';
import styled from 'styled-components';
import last from 'lodash/last';
import PropTypes from 'prop-types';

Here is the implementation of TreeNode:

const getNodeLabel = (node) => last(node.path.split('/'));

const TreeNode = (props) => {
  const { node, getChildNodes, level, onToggle } = props;

  return (
    <React.Fragment>
      <div level={level} type={node.type}>
        <div onClick={() => onToggle(node)}>
          { node.type === 'folder' && (node.isOpen ? <FaChevronDown /> : <FaChevronRight />) }
        </div>
        
        <div marginRight={10}>
          { node.type === 'file' && <FaFile /> }
          { node.type === 'folder' && node.isOpen === true && <FaFolderOpen /> }
          { node.type === 'folder' && !node.isOpen && <FaFolder /> }
        </div>
        

        <span role="button">
          { getNodeLabel(node) }
        </span>
      </div>

      { node.isOpen && getChildNodes(node).map(childNode => (
        <TreeNode 
          {...props}
          node={childNode}          
          level={level + 1}
        />
      ))}
    </React.Fragment>
  );
}

TreeNode.propTypes = {
  node: PropTypes.object.isRequired,
  getChildNodes: PropTypes.func.isRequired,
  level: PropTypes.number.isRequired,
  onToggle: PropTypes.func.isRequired,  
};

TreeNode.defaultProps = {
  level: 0,
};

export default TreeNode;

First, we define getNodeLabel function. This function splits node.path and returns the last segment. Eg: /path/to/myfile.json becomes myfile.js

When node.type equals “folder” and it is opened, we render arrow right icon, otherwise, we render arrow down icon. We don’t render the arrow if it is not “folder”.

Next, we render the file/folder icons.

And then we call getNodeLabel and display node label.

Finally, if a folder is opened, we find its child nodes and render them. This process is repeated itself utils there is no children.

Every time we render child nodes, level will be increased by 1. We will use level props to calculate padding-left of the NodeItem.

Now we can import TreeNode component inside Tree.js. After that, we put Tree component in the render method of App.js. Our React app will be reloaded by itself and we will see something like this:

Make our component beautiful by styled-components

Let’s add the following code just right below the import statements:

const getPaddingLeft = (level, type) => {
  let paddingLeft = level * 20;
  if (type === 'file') paddingLeft += 20;
  return paddingLeft;
}

const StyledTreeNode = styled.div`
  display: flex;
  flex-direction: row;
  align-items: center;
  padding: 5px 8px;
  padding-left: ${props => getPaddingLeft(props.level, props.type)}px;

  &:hover {
    background: lightgray;
  }
`;

const NodeIcon = styled.div`
  font-size: 12px;
  margin-right: ${props => props.marginRight ? props.marginRight : 5}px;
`;

We’ve just created 2 styled components using styled-components. As you can see, the padding-left property is calculated by getPaddingLeft function. So higher level NodeItem will have bigger indentation.

Let’s put these components into render method of TreeNode:

const TreeNode = (props) => {
  const { node, getChildNodes, level, onToggle } = props;

  return (
    <React.Fragment>
      <StyledTreeNode level={level} type={node.type}>
        <NodeIcon onClick={() => onToggle(node)}>
          { node.type === 'folder' && (node.isOpen ? <FaChevronDown /> : <FaChevronRight />) }
        </NodeIcon>
        
        <NodeIcon marginRight={10}>
          { node.type === 'file' && <FaFile /> }
          { node.type === 'folder' && node.isOpen === true && <FaFolderOpen /> }
          { node.type === 'folder' && !node.isOpen && <FaFolder /> }
        </NodeIcon>
        

        <span role="button">
          { getNodeLabel(node) }
        </span>
      </StyledTreeNode>

      { node.isOpen && getChildNodes(node).map(childNode => (
        <TreeNode 
          {...props}
          node={childNode}          
          level={level + 1}
        />
      ))}
    </React.Fragment>
  );
}

Display child nodes

Right now there is no child node is rendered because isOpen property on each node is still undefined. Let’s go back to Tree.js and add this method:

onToggle = (node) => {
  const { nodes } = this.state;
  nodes[node.path].isOpen = !node.isOpen;
  this.setState({ nodes });
}

When onToggle is called, it switches the value of isOpen in the node object and also updates the state of Tree component.

We need to put this method into TreeNode.

render() {
   const rootNodes = this.getRootNodes();
   return (
     <div>
       { rootNodes.map(node => (
         <TreeNode 
           node={node}
           getChildNodes={this.getChildNodes}
           onToggle={this.onToggle}            
         />
       ))}
     </div>
   )
 }

Return to our website and we can see the result:

Thanks for reading, I hope this tutorial is helpful for you.