DEV Community

Cover image for Managing Recursive Components in React With Custom Hook
Mohammad Khayata
Mohammad Khayata

Posted on • Originally published at linkedin.com

Managing Recursive Components in React With Custom Hook

When developing user interfaces, especially complex ones, you'll often encounter scenarios that require rendering nested structures. Components such as Tree Views, file systems, routing systems, collapsible menus, and nested lists all share a common trait: they rely on recursive components. Understanding how recursive components work can greatly enhance your ability to create flexible, dynamic, and reusable components in your applications.

Note: This article is not about building a simple TreeView with React. Instead, it focuses on learning how to manage recursive component state using a custom hook based on useState.

What Are Recursive Components?

A recursive component is a React component that calls itself as part of its render method. This pattern is particularly useful when dealing with data structures that are hierarchical or nested by nature. By leveraging recursion, a component can handle data of arbitrary depth, making it a powerful tool for rendering complex structures like trees or directories.

In this article, we will build a simple TreeView component demo using a custom hook based on *useState * to manage the state of the tree.

Irecursive component using custom react hook based on useState

Bulding The useTree Hook

the hook internaly will be based on useState be cause we need our componentes to be reactive !

The useTree hook is a custom React hook designed to manage a tree data structure with various utility functions for updating, traversing, and interacting with the nodes. Hereโ€™s a step-by-step breakdown of how this hook works:

Because the code is a bit long, I will only put the function titles in this article, and you can get the full code from the GitHub Repo

Initial Setup and Types

Interfaces and Types:
BaseNodeProps: Defines default properties for a tree node, such as level, isOpen, isLoading, isActive, etc.
TreeNode: A generic interface defining a tree node that includes an id, title, data, props, and potentially childrens which are nested nodes.
ToTreeOptions: Specifies the configuration for converting a flat data structure into a tree, including the keys for id, title, childrens, and an optional function setProps to customize node properties.

export interface BaseNodeProps { ... }
export interface TreeNode<TData = unknown, TProps extends BaseNodeProps = BaseNodeProps> { ... }
export interface ToTreeOptions<TData, TProps> { ... }
Enter fullscreen mode Exit fullscreen mode

Tree Conversion Function

toTree Function:
A recursive function that converts an array of data into a tree structure.
It takes data and options, then maps each item to a TreeNode.
For each node, it checks if it has children (childrens). If so, it recursively converts them into TreeNode as well

export const toTree = <TData, TProps extends BaseNodeProps = BaseNodeProps>(
  data: TData[],
  { idKey, titleKey, childsKey, setProps = ..., initialIndex = -1 }: ToTreeOptions<TData, TProps>
): TreeNode<TData, TProps>[] => { ... }
Enter fullscreen mode Exit fullscreen mode

Hook Initialization

useTree Hook:
State Initialization:
Uses useState to create a state tree initialized by converting initialValue using the toTree function and treeNodeOptions.

const [tree, setTree] = useState<TreeNode<TData, TProps>[]>(
  toTree(initialValue, treeNodeOptions)
);
Enter fullscreen mode Exit fullscreen mode

Utility Functions for Tree Manipulation

const updateNode = (
  nodeId: number | string,
  matchedUpdater: UpdateNodePayload,
  notMatchedUpdater?: UpdateNodePayload
) => { ... }

const setNodeChilds = (
  nodeId: number | string,
  childrens: TreeNode<TData, TProps>[]
) => { ... }

const setNodeProps = (
  id: number | string,
  matchedUpdater: UpdateNodePropsPayload,
  notMatchedUpdater?: UpdateNodePropsPayload
) => { ... }

const setNodeOpen = (nodeId: number | string, is?: boolean) => { ... }

const setActiveNode = (id: number | string, is: boolean) => { ... }

Enter fullscreen mode Exit fullscreen mode

Utility Functions for Tree Navigation and Queries

Memoized arrays of node IDs that are open or loading, respectively.

const getNodeParent = (
  nodeId: number | string,
  subTree: TreeNode<TData, TProps>[]
): TreeNode<TData, TProps> | null => { ... }

const findNodeById = (
  finder: (node: TreeNode<TData, TProps>) => boolean,
  nodes: TreeNode<TData, TProps>[] = tree
): TreeNode<TData, TProps> | null => { ... }

const traverseTree = (
  callback: (node: TreeNode<TData, TProps>) => void,
  nodesProp?: TreeNode<TData, TProps>[]
) => { ... }

const filterTree = (callback: (node: TreeNode<TData, TProps>) => boolean) => { ... }

const getNodeParents = (nodeId: number | string): TreeNode<TData, TProps>[] => { ... }
Enter fullscreen mode Exit fullscreen mode

Derived States

Memoized arrays of node IDs that are open or loading, respectively.

const openedNodes = useMemo(() => tree.filter((node) => node.props.isOpen).map((node) => node.id), [tree]);
const loadingNodes = useMemo(() => tree.filter((node) => node.props.isLoading).map((node) => node.id), [tree]);
Enter fullscreen mode Exit fullscreen mode

so The hook returns various functions and states to interact with and manipulate the tree, such as initializing the tree, setting node children, updating nodes, finding nodes, and more.

return {
  initializeTree,
  setNodeChilds,
  findNodeById,
  traverseTree,
  setNodeProps,
  setActiveNode,
  setNodeOpen,
  toTree,
  getNodeParent,
  getNodeParents,
  filterTree,
  openedNodes,
  loadingNodes,
  tree,
};
Enter fullscreen mode Exit fullscreen mode

Usage Example With By Building Basic TreeView Demo

Code Overview
Imports:
The code imports React and several utilities from a custom useTree hook module: useTree, BaseNodeProps, ToTreeOptions, and TreeNode.

Data Definition

A sample data structure, data, is defined to represent the tree. This data includes a list of root nodes, each with an ID, a name, and an optional list of child nodes (employees). The child nodes can have their own children, creating a recursive tree structure.

TreeView Component:

TreeView is a recursive component that renders the tree nodes.
It accepts two props: nodes (the array of tree nodes to display) and toggleNodeOpen (a function to toggle a node's open/closed state).
Inside the component, it iterates over each node and renders it. For each node, it displays a toggle icon (+ or -), the node title, and, if the node is open (isOpen is true), it recursively renders its children by calling TreeView again.

TreeViewDemo Component:

This is the main component that initializes and displays the tree.
It defines treeOptions, an object specifying how to map the data to tree nodes. This object tells the useTree hook which keys in the data correspond to the node ID, title, and children.
The useTree hook is called with the initial data and treeOptions to create and manage the tree state. The hook returns tree, the current tree state, and setNodeProps, a function to update node properties.
The toggleNodeOpen function is defined to toggle the isOpen property of a node, allowing nodes to be expanded or collapsed. It uses setNodeProps to update the node state.
Finally, TreeViewDemo renders the TreeView component, passing it the tree nodes (tree) and the toggle function (toggleNodeOpen).

import React from "react";
import {
  useTree,
  BaseNodeProps,
  ToTreeOptions,
  TreeNode,
} from "../hooks/useTree"; // Adjust the import path as needed

const data = [
  {
    id: 1,
    name: "Root 1",
    employees: [
      { id: 2, name: "Child 1", employees: [] },
      { id: 3, name: "Child 2", employees: [] },
    ],
  },
  {
    id: 4,
    name: "Root 2",
    employees: [
      {
        id: 5,
        name: "Child 3",
        employees: [{ id: 6, name: "Subchild 1", employees: [] }],
      },
    ],
  },
];

const TreeView = <TData, TProps extends BaseNodeProps = BaseNodeProps>({
  nodes,
  toggleNodeOpen,
}: {
  nodes: TreeNode<TData, TProps>[];
  toggleNodeOpen: (nodeId: number | string) => void;
}) => {
  return (
    <ul className="tree-list">
      {nodes.map((node) => (
        <li key={node.id} className="tree-item">
          {/* Render node title */}
          <div onClick={() => toggleNodeOpen(node.id)} className="tree-node">
            <span className="toggle-icon">{node.props.isOpen ? "-" : "+"}</span>
            <span className="node-title">{node.title}</span>
          </div>
          {/* Recursively render children if node is open */}
          {node.props.isOpen && node.childrens && (
            <TreeView nodes={node.childrens} toggleNodeOpen={toggleNodeOpen} />
          )}
        </li>
      ))}
    </ul>
  );
};

const TreeViewDemo: React.FC = () => {
  const treeOptions: ToTreeOptions<(typeof data)[0], BaseNodeProps> = {
    idKey: "id",
    titleKey: "name",
    childsKey: "employees",
  };

  // Initialize the tree hook
  const { tree, setNodeProps } = useTree(data, treeOptions);

  // Toggle node open/closed
  const toggleNodeOpen = (nodeId: number | string) => {
    setNodeProps(nodeId, ({ props }) => ({
      ...props,
      isOpen: !props.isOpen,
    }));
  };

  return (
    <div className="tree-container">
      {/* Render the tree view recursively */}
      <TreeView nodes={tree} toggleNodeOpen={toggleNodeOpen} />
    </div>
  );
};

export default TreeViewDemo;

Enter fullscreen mode Exit fullscreen mode

Here is the full code with example on stackblitz :

Or you can clone the repo from Github

Top comments (0)