Tree Data in React Tables: The Complete Guide to Hierarchical Data Display

Tree DataTutorialHierarchical Data

Organization charts, file systems, project hierarchies—hierarchical data is everywhere. Learn how to display tree structures in React tables with expandable rows, lazy loading, and performance optimization.

You're building an org chart. Each company has divisions. Each division has departments. Each department has teams. Users need to drill down through this hierarchy, expanding and collapsing as they explore. Displaying this kind of nested, parent-child data in a table is called tree data or hierarchical data display.

Unlike flat tables where every row is independent, tree data requires understanding relationships: which rows are parents, which are children, how deep the nesting goes, and which branches are expanded or collapsed. Get it wrong, and you end up with a confusing mess. Get it right, and users can navigate complex data structures intuitively.

In this guide, we'll cover how to implement tree data in React tables, common patterns for structuring hierarchical data, performance considerations for deep nesting, and advanced techniques like lazy loading child nodes on demand.

Why Tree Data Matters

Hierarchical data is everywhere in business applications:

Organization Charts

Companies → Divisions → Departments → Teams → Employees. Multiple levels of reporting structure need expandable visualization.

Project Management

Projects → Milestones → Tasks → Subtasks. Track progress across nested work items with clear parent-child relationships.

Category Hierarchies

Product catalogs, file systems, menu navigation—any data with categories, subcategories, and items.

Financial Reporting

Accounts → Sub-accounts → Transactions. Display nested financial data with rollup totals at each level.

Without proper tree data support, developers resort to hacky workarounds: separate tables for each level, custom expand/collapse logic, manual indentation with CSS. These approaches break down with deep nesting or dynamic data. That's why full-featured data grids include built-in row grouping.

Understanding Tree Data Structure

Tree data in React tables uses nested arrays to represent parent-child relationships. Each parent row contains an array of child rows, which can themselves contain arrays of grandchildren, and so on.

Simple Two-Level Example

React TSX
1// Companies → Departments (2 levels)
2const data = [
3 {
4 id: "company-1",
5 name: "TechCorp",
6 employees: 250,
7 revenue: "$25M",
8 // Children stored in 'departments' array
9 departments: [
10 {
11 id: "dept-1",
12 name: "Engineering",
13 employees: 120,
14 revenue: "$15M",
15 },
16 {
17 id: "dept-2",
18 name: "Sales",
19 employees: 80,
20 revenue: "$8M",
21 },
22 {
23 id: "dept-3",
24 name: "Marketing",
25 employees: 50,
26 revenue: "$2M",
27 },
28 ],
29 },
30 {
31 id: "company-2",
32 name: "HealthPlus",
33 employees: 180,
34 revenue: "$18M",
35 departments: [
36 {
37 id: "dept-4",
38 name: "Medical",
39 employees: 100,
40 revenue: "$12M",
41 },
42 {
43 id: "dept-5",
44 name: "Research",
45 employees: 80,
46 revenue: "$6M",
47 },
48 ],
49 },
50];

Multi-Level Deep Nesting

React TSX
1// Companies → Divisions → Departments → Teams (4 levels)
2const data = [
3 {
4 id: "company-1",
5 name: "TechCorp",
6 employees: 500,
7 // Level 1: Divisions
8 divisions: [
9 {
10 id: "div-1",
11 name: "Engineering Division",
12 employees: 300,
13 // Level 2: Departments
14 departments: [
15 {
16 id: "dept-1",
17 name: "Frontend Department",
18 employees: 100,
19 // Level 3: Teams
20 teams: [
21 {
22 id: "team-1",
23 name: "React Team",
24 employees: 30,
25 },
26 {
27 id: "team-2",
28 name: "Vue Team",
29 employees: 25,
30 },
31 ],
32 },
33 {
34 id: "dept-2",
35 name: "Backend Department",
36 employees: 120,
37 teams: [
38 {
39 id: "team-3",
40 name: "API Team",
41 employees: 40,
42 },
43 {
44 id: "team-4",
45 name: "Database Team",
46 employees: 35,
47 },
48 ],
49 },
50 ],
51 },
52 ],
53 },
54];

Key Pattern

Each level of the hierarchy uses a different array property name (e.g., divisions, departments, teams). This tells the table which property contains the next level of children.

Implementation with Simple Table

Step 1: Mark the Expandable Column

First, tell Simple Table which column should show the expand/collapse controls:

React TSX
1import { SimpleTable, HeaderObject } from "simple-table-core";
2import "simple-table-core/styles.css";
3
4const headers: HeaderObject[] = [
5 {
6 accessor: "name",
7 label: "Organization Name",
8 width: 250,
9 expandable: true, // This column gets expand/collapse controls
10 type: "string",
11 },
12 {
13 accessor: "employees",
14 label: "Employees",
15 width: 120,
16 type: "number",
17 },
18 {
19 accessor: "revenue",
20 label: "Revenue",
21 width: 150,
22 type: "string",
23 },
24];

Step 2: Define the Hierarchy Levels

Use the rowGrouping prop to specify the array property names for each nesting level:

React TSX
1// Two levels: Companies → Departments
2<SimpleTable
3 defaultHeaders={headers}
4 rows={data}
5 rowIdAccessor="id"
6 rowGrouping={["departments"]} // Array property name for children
7/>
8
9// Three levels: Companies → Divisions → Departments
10<SimpleTable
11 defaultHeaders={headers}
12 rows={data}
13 rowIdAccessor="id"
14 rowGrouping={["divisions", "departments"]} // Order = nesting depth
15/>
16
17// Four levels: Companies → Divisions → Departments → Teams
18<SimpleTable
19 defaultHeaders={headers}
20 rows={data}
21 rowIdAccessor="id"
22 rowGrouping={["divisions", "departments", "teams"]}
23/>

How it Works

Simple Table reads the rowGrouping array to understand your hierarchy:

  • First element ("divisions") = Level 1 children
  • Second element ("departments") = Level 2 children
  • Third element ("teams") = Level 3 children

Step 3: Control Initial Expand State

React TSX
1// Start with all rows collapsed
2<SimpleTable
3 expandAll={false}
4 // ... other props
5/>
6
7// Start with all rows expanded (default)
8<SimpleTable
9 expandAll={true}
10 // ... other props
11/>

Complete Example

React TSX
1import { SimpleTable, HeaderObject } from "simple-table-core";
2import "simple-table-core/styles.css";
3
4const headers: HeaderObject[] = [
5 {
6 accessor: "organization",
7 label: "Organization",
8 width: 200,
9 expandable: true,
10 type: "string"
11 },
12 { accessor: "employees", label: "Employees", width: 100, type: "number" },
13 { accessor: "budget", label: "Budget", width: 140, type: "string" },
14 { accessor: "location", label: "Location", width: 130, type: "string" },
15];
16
17const data = [
18 {
19 id: "company-1",
20 organization: "TechSolutions Inc.",
21 employees: 137,
22 budget: "$15.0M",
23 location: "San Francisco",
24 divisions: [
25 {
26 id: "div-100",
27 organization: "Engineering Division",
28 employees: 97,
29 budget: "$10.6M",
30 location: "Multiple",
31 departments: [
32 {
33 id: "dept-1001",
34 organization: "Frontend",
35 employees: 28,
36 budget: "$2.8M",
37 location: "San Francisco",
38 },
39 {
40 id: "dept-1002",
41 organization: "Backend",
42 employees: 32,
43 budget: "$3.4M",
44 location: "Seattle",
45 },
46 ],
47 },
48 {
49 id: "div-101",
50 organization: "Product Division",
51 employees: 40,
52 budget: "$4.4M",
53 location: "Multiple",
54 departments: [
55 {
56 id: "dept-1101",
57 organization: "Design",
58 employees: 17,
59 budget: "$1.8M",
60 location: "Portland",
61 },
62 ],
63 },
64 ],
65 },
66];
67
68export default function OrgChartTable() {
69 return (
70 <SimpleTable
71 defaultHeaders={headers}
72 rows={data}
73 rowIdAccessor="id"
74 rowGrouping={["divisions", "departments"]}
75 expandAll={false}
76 height="600px"
77 />
78 );
79}

Lazy Loading: Load Children On-Demand

For large hierarchies, loading all data upfront is slow and memory-intensive. Instead, load only top-level rows initially, then fetch children when users expand a parent. This pattern works especially well with pagination for even better performance.

Using onRowGroupExpand Callback

React TSX
1import { SimpleTable, HeaderObject, OnRowGroupExpandProps } from "simple-table-core";
2import { useState } from "react";
3
4export default function LazyTreeTable() {
5 const [data, setData] = useState([
6 // Initial data: only top-level rows, no children yet
7 {
8 id: "region-1",
9 name: "North America",
10 count: 150,
11 // No 'stores' array yet - will be loaded on expand
12 },
13 {
14 id: "region-2",
15 name: "Europe",
16 count: 200,
17 },
18 ]);
19
20 const handleExpand = async ({
21 row,
22 rowId,
23 depth,
24 groupingKey,
25 isExpanded,
26 setLoading,
27 setError,
28 setEmpty,
29 rowIndexPath,
30 }: OnRowGroupExpandProps) => {
31 // Only load when expanding (not collapsing)
32 if (!isExpanded) return;
33
34 // Set loading state in the UI
35 setLoading(true);
36
37 try {
38 // Fetch children from API
39 const response = await fetch(`/api/tree-data?parent=${rowId}&level=${groupingKey}`);
40 const children = await response.json();
41
42 setLoading(false);
43
44 if (children.length === 0) {
45 setEmpty(true, "No items found");
46 return;
47 }
48
49 // Update data with children
50 setData((prevData) => {
51 const newData = [...prevData];
52 // rowIndexPath provides exact location: [0] means first row
53 newData[rowIndexPath[0]][groupingKey] = children;
54 return newData;
55 });
56 } catch (error) {
57 setLoading(false);
58 setError("Failed to load data");
59 }
60 };
61
62 return (
63 <SimpleTable
64 defaultHeaders={headers}
65 rows={data}
66 rowIdAccessor="id"
67 rowGrouping={["stores", "products"]}
68 onRowGroupExpand={handleExpand}
69 loadingStateRenderer="Loading..."
70 errorStateRenderer="Failed to load"
71 emptyStateRenderer="No data available"
72 />
73 );
74}

Benefits of Lazy Loading

  • Faster Initial Load: Only fetch top-level data, not entire tree
  • Reduced Memory: Children only loaded when needed
  • Better Performance: Scales to massive hierarchies (1000s of nodes)
  • Seamless UX: Loading states keep users informed

rowIndexPath: Simplified Updates

The rowIndexPath array tells you exactly where to insert children:

React TSX
1// Example: Expanding "Backend" department (3rd item in divisions array)
2// rowIndexPath = [0, "divisions", 2]
3// Meaning: rows[0].divisions[2] = Backend department
4
5// Update is simple:
6setData(prev => {
7 const newData = [...prev];
8 newData[rowIndexPath[0]][groupingKey] = children;
9 return newData;
10});
11
12// For deeper nesting, traverse the path:
13// rowIndexPath = [0, "divisions", 1, "departments", 2]
14// Meaning: rows[0].divisions[1].departments[2]
15
16let target = newData[rowIndexPath[0]];
17for (let i = 1; i < rowIndexPath.length - 1; i += 2) {
18 const key = rowIndexPath[i];
19 const index = rowIndexPath[i + 1];
20 target = target[key][index];
21}
22target[groupingKey] = children;

Common Use Cases & Patterns

Organization Chart

React TSX
1// Structure: Company → Divisions → Departments → Teams
2rowGrouping={["divisions", "departments", "teams"]}
3
4// Example Row
5{
6 id: "company-1",
7 name: "TechCorp",
8 headcount: 500,
9 divisions: [
10 {
11 id: "div-1",
12 name: "Engineering",
13 headcount: 300,
14 departments: [...]
15 }
16 ]
17}

Project Management

React TSX
1// Structure: Projects → Milestones → Tasks → Subtasks
2rowGrouping={["milestones", "tasks", "subtasks"]}
3
4// Example Row
5{
6 id: "project-1",
7 name: "Website Redesign",
8 status: "In Progress",
9 milestones: [
10 {
11 id: "milestone-1",
12 name: "Design Phase",
13 progress: "80%",
14 tasks: [
15 {
16 id: "task-1",
17 name: "Create Wireframes",
18 assignee: "Sarah",
19 subtasks: [...]
20 }
21 ]
22 }
23 ]
24}

Product Catalog

React TSX
1// Structure: Categories → Subcategories → Products
2rowGrouping={["subcategories", "products"]}
3
4// Example Row
5{
6 id: "cat-1",
7 name: "Electronics",
8 itemCount: 1250,
9 subcategories: [
10 {
11 id: "subcat-1",
12 name: "Laptops",
13 itemCount: 145,
14 products: [
15 {
16 id: "prod-1",
17 name: "MacBook Pro 16",
18 price: "$2,499",
19 stock: 42
20 }
21 ]
22 }
23 ]
24}

Financial Accounts

React TSX
1// Structure: Accounts → Sub-accounts → Transactions
2rowGrouping={["subaccounts", "transactions"]}
3
4// Example Row
5{
6 id: "account-1",
7 name: "Operating Account",
8 balance: "$125,000",
9 subaccounts: [
10 {
11 id: "subaccount-1",
12 name: "Payroll",
13 balance: "$85,000",
14 transactions: [
15 {
16 id: "trans-1",
17 date: "2025-12-01",
18 description: "Employee Salaries",
19 amount: "-$75,000"
20 }
21 ]
22 }
23 ]
24}

Performance Optimization Tips

✅ Use Lazy Loading for Deep Hierarchies

Don't load all 10,000 nodes upfront. Use onRowGroupExpand to fetch children only when parents are expanded. Initial load stays fast even with massive trees.

✅ Start Collapsed for Large Trees

Set expandAll= when you have 100+ top-level rows or deep nesting (4+ levels). Let users expand what they need rather than rendering thousands of rows at once.

✅ Virtualization Is Built-In

Simple Table's virtualization works with tree data. Even if you expand to 5,000 visible rows, only ~30 DOM nodes are rendered at any time. Scrolling remains smooth.

✅ Stable Row IDs Are Critical

Use unique, stable IDs for every row at every level (rowIdAccessor). This prevents React re-renders when expanding/collapsing and keeps expand state consistent.

❌ Avoid: Loading Entire Tree Upfront

Don't fetch all levels of a 10,000-node tree on page load. Use lazy loading. Your backend should support /api/tree?parent=123 to fetch children on demand.

❌ Avoid: Complex Transforms in Render

Don't flatten/transform tree data inside your component render. Do it once when data loads, then pass the structured tree to Simple Table. Repeating transforms on every render tanks performance.

Tree Data Made Simple

Tree data doesn't have to be complicated. With the right structure—nested arrays with clear property names—and the right API—expandable columns and rowGrouping configuration—hierarchical data becomes as easy to display as flat tables.

Key takeaways:

  • Structure data with nested arrays using clear property names for each hierarchy level
  • Mark one column as expandable to show expand/collapse controls
  • Define hierarchy with rowGrouping array to tell the table which properties contain children
  • Use lazy loading for large trees via onRowGroupExpand to load children on demand
  • Start collapsed for deep hierarchies with expandAll=

Whether you're building org charts, project trackers, file explorers, or financial reports, tree data is a fundamental pattern. With Simple Table's row grouping, you get expandable hierarchies out of the box—no custom logic, no complex state management, just structured data and clear configuration.

Ready to display hierarchical data?

Simple Table makes tree data intuitive with built-in row grouping, lazy loading, and expand/collapse controls. No custom state management required.