React Tree Viewer: Event Flow, State Design, and Reusability

Faiaz KhanFaiaz Khan
4 min read

If you’ve ever tried to build a Tree Viewer in React, you know it’s like trying to herd cats with children. It starts innocent enough: a list of folders, maybe some nested files. Then someone says, “Can you make it expandable?“ Then: “We need to drag and drop,“ and finally: “Let’s reuse this in five different places.“ Oh. Oh no.

This post is about how to architect a Tree Viewer in React so it doesn’t become an unmaintainable nightmare. We’ll walk through:

  • Recursive component structure

  • Local vs global state decisions

  • Handling events that bubble from leaf to root

  • How to keep it reusable, predictable, and production-ready

Let’s go.


Why a Tree UI is Tricky in React

Trees are inherently recursive, but React components don’t magically recurse. You have to do the recursion yourself — rendering the same component inside itself.

That leads to questions like:

  • Where does state live?

  • How do you handle click events for a deeply nested node?

  • How do you make sure each node can be controlled independently and communicate upwards?


Step 1: Recursive Component Structure

Here’s the minimal structure for a Tree Viewer:

type TreeNode = {
  id: string;
  name: string;
  children?: TreeNode[];
};

const TreeItem: React.FC<{ node: TreeNode }> = ({ node }) => {
  const [expanded, setExpanded] = useState(false);

  const toggle = () => setExpanded(!expanded);

  return (
    <div className="ml-4">
      <div onClick={toggle}>
        {node.name} {node.children && (expanded ? "[-]" : "[+]")}
      </div>
      {expanded && node.children?.map((child) => (
        <TreeItem key={child.id} node={child} />
      ))}
    </div>
  );
};

What this does well:

  • Elegant recursion

  • Independent expanded state per node

  • Works out of the box for small trees

What it doesn’t do well:

  • No event bubbling

  • State isn’t liftable

  • Can’t control nodes from outside

  • Not testable or reusable in more advanced scenarios


Step 2: Designing Event Flow Like a Pro

We want every node to be able to:

  • Tell it’s parent that it was clicked

  • Receive props to control its state externally, if needed

  • Be fully reusable without knowing about the whole tree

So we’ll lift state and events upward using a map structure:

type TreeState = Record<string, boolean>; // id -> expanded state

We’ll keep this in a parent component, and pass down props;

const TreeItem: React.FC<{
  node: TreeNode;
  expandedMap: TreeState;
  onToggle: (id: string) => void;
}> = ({ node, expandedMap, onToggle }) => {
  const isExpanded = expandedMap[node.id] || false;

  return (
    <div className="ml-4">
      <div onClick={() => onToggle(node.id)}>
        {node.name} {node.children && (isExpanded ? "[-]" : "[+]")}
      </div>
      {isExpanded && node.children?.map((child) => (
        <TreeItem
          key={child.id}
          node={child}
          expandedMap={expandedMap}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
};

And then control all the logic from the top:

const TreeViewer = ({ tree }: { tree: TreeNode[] }) => {
  const [expandedMap, setExpandedMap] = useState<TreeState>({});

  const handleToggle = (id: string) => {
    setExpandedMap((prev) => ({
      ...prev,
      [id]: !prev[id],
    }));
  };

  return tree.map((node) => (
    <TreeItem
      key={node.id}
      node={node}
      expandedMap={expandedMap}
      onToggle={handleToggle}
    />
  ));
};

Result?

  • One source of truth

  • All state is centralized and testable

  • Every node is dumb and reusable


Step 3: Adding Reusability and Extensibility

To make this Tree Viewer production-level, we need:

  • Custom renderers (renderLabel, renderIcon, etc.)

  • Optional behaviors (expand on click, lazy load children, etc.)

  • Full control for consumers

Here’s how we can make each TreeItem more reusable:

interface TreeItemProps {
  node: TreeNode;
  expandedMap: TreeState;
  onToggle: (id: string) => void;
  renderLabel?: (node: TreeNode, expanded: boolean) => React.ReactNode;
}

const TreeItem: React.FC<TreeItemProps> = ({
  node,
  expandedMap,
  onToggle,
  renderLabel,
}) => {
  const isExpanded = expandedMap[node.id] || false;

  return (
    <div className="ml-4">
      <div onClick={() => onToggle(node.id)}>
        {renderLabel ? renderLabel(node, isExpanded) : node.name}
      </div>
      {isExpanded && node.children?.map((child) => (
        <TreeItem
          key={child.id}
          node={child}
          expandedMap={expandedMap}
          onToggle={onToggle}
          renderLabel={renderLabel}
        />
      ))}
    </div>
  );
};

Now the consumer of TreeViewer can fully customize the label rendering:

<TreeViewer
  tree={myData}
  renderLabel={(node, expanded) => (
    <span>
      {expanded ? "📂" : "📁"} {node.name}
    </span>
  )}
/>

Clean, testable, reusable.


State Design Lessons from Tree UIs

Building a Tree Viewer teaches you:

  • Lift state if you need control, don’t fear prop-drilling

  • Push logic upward, make components dumb

  • Recursive componentscomplex code, if you decouple rendering from logic

  • Extensibility requires of control: let the caller customize, don’t assume


Final Thoughts

If your Tree Viewer isn’t testable, predictable, or reusable, it’s not ready for production—no matter how cute the icons are. Architecting it properly gives you control, consistency, and that sweet “I did this right“ feeling.

And remember: recursive problems aren’t hard—just misunderstood.

0
Subscribe to my newsletter

Read articles from Faiaz Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Faiaz Khan
Faiaz Khan

Hey! I'm Faiaz — a frontend developer who loves writing clean, efficient, and readable code (and sometimes slightly chaotic code, but only when debugging). This blog is my little corner of the internet where I share what I learn about React, JavaScript, modern web tools, and building better user experiences. When I'm not coding, I'm probably refactoring my to-do list or explaining closures to my cat. Thanks for stopping by — hope you find something useful or mildly entertaining here.