Overview
React Apple Tree provides a comprehensive set of utility functions for tree manipulation. All operations are immutable and return new tree structures.
Import utility functions from the main package:import {
addNodeUnderParent,
removeNode,
changeNodeAtPath
} from 'react-apple-tree';
Adding Nodes
Add Under Parent
Add a new node as a child of a specific parent:
import { addNodeUnderParent } from 'react-apple-tree';
const { treeData: updatedTree, treeIndex } = addNodeUnderParent({
treeData: currentTree,
parentKey: 'parent-1', // null for root level
newNode: {
id: 'new-node',
title: 'New Child'
},
getNodeKey: ({ node }) => node.id,
expandParent: true, // Expand parent after adding
addAsFirstChild: false // Add at end (true for beginning)
});
setTreeData(updatedTree);
treeData
Array<TreeItem<T>>
required
The current tree data
Key of parent node. Use null to add at root level
Function to extract node keys
Whether to expand parent after insertion
Add as first child (true) or last child (false)
Insert at Depth and Index
Insert a node at a specific depth and position:
import { insertNode } from 'react-apple-tree';
const { treeData: newTree, treeIndex, path, parentNode } = insertNode({
treeData: currentTree,
depth: 2, // Depth level (0 = root)
minimumTreeIndex: 5, // Insert after this index
newNode: {
id: 'new-node',
title: 'Inserted Node'
},
getNodeKey: ({ node }) => node.id,
ignoreCollapsed: true,
expandParent: false
});
Removing Nodes
Remove by Path
Remove a node and get information about the removal:
import { removeNode } from 'react-apple-tree';
const { treeData: newTree, node: removedNode, treeIndex } = removeNode({
treeData: currentTree,
path: ['root', 'folder-1', 'file-to-delete'],
getNodeKey: ({ node }) => node.id,
ignoreCollapsed: true
});
console.log('Removed:', removedNode.title);
setTreeData(newTree);
Removing a parent node will also remove all its descendants.
Remove Only Node
If you only need the updated tree without removal details:
import { removeNodeAtPath } from 'react-apple-tree';
const newTree = removeNodeAtPath({
treeData: currentTree,
path: ['root', 'node-to-delete'],
getNodeKey: ({ node }) => node.id
});
Updating Nodes
Change Node at Path
Update a specific node by its path:
import { changeNodeAtPath } from 'react-apple-tree';
// Replace with new node
const updatedTree = changeNodeAtPath({
treeData: currentTree,
path: ['root', 'folder-1', 'file-1'],
newNode: {
id: 'file-1',
title: 'Updated Title',
subtitle: 'Modified just now'
},
getNodeKey: ({ node }) => node.id
});
// Or use a function to update based on current node
const updatedTree2 = changeNodeAtPath({
treeData: currentTree,
path: ['root', 'counter'],
newNode: ({ node, treeIndex }) => ({
...node,
count: (node.count || 0) + 1
}),
getNodeKey: ({ node }) => node.id
});
Static Update
Dynamic Update
Delete
// Replace with a new node object
changeNodeAtPath({
treeData,
path: ['1', '1-1'],
newNode: { id: '1-1', title: 'New Title' },
getNodeKey: ({ node }) => node.id
});
// Update based on current node
changeNodeAtPath({
treeData,
path: ['1', '1-1'],
newNode: ({ node }) => ({
...node,
title: node.title + ' (edited)'
}),
getNodeKey: ({ node }) => node.id
});
// Return null to delete the node
changeNodeAtPath({
treeData,
path: ['1', '1-1'],
newNode: null, // Deletes the node
getNodeKey: ({ node }) => node.id
});
Get Node at Path
Retrieve node information by path:
import { getNodeAtPath } from 'react-apple-tree';
const nodeInfo = getNodeAtPath({
treeData: currentTree,
path: ['root', 'folder-1', 'file-1'],
getNodeKey: ({ node }) => node.id,
ignoreCollapsed: true
});
if (nodeInfo) {
console.log('Node:', nodeInfo.node);
console.log('Index:', nodeInfo.treeIndex);
}
Moving Nodes
Nodes are automatically moved during drag-and-drop operations. The internal function:
src/utils/node-operations.ts
export function moveNodeToDifferentParent(
treeData: Array<TreeItem>,
treeMap: TreeMap,
nodeKey: NodeKey,
prevParentKey: NodeKey | null,
nextParentKey: NodeKey | null,
siblingIndex: number,
getNodeKey: GetNodeKeyFn,
): Array<TreeItem>
You typically don’t call this directly. Instead, use the onMoveNode callback to respond to moves:<ReactAppleTree
treeData={data}
onChange={setData}
getNodeKey={({ node }) => node.id}
onMoveNode={(data) => {
console.log('Node moved:', data.node.title);
console.log('From:', data.prevPath);
console.log('To:', data.nextPath);
}}
/>
Expanding and Collapsing
Toggle Single Node
Expand or collapse by updating the expanded property:
const toggleNode = (path: string[]) => {
const updated = changeNodeAtPath({
treeData,
path,
newNode: ({ node }) => ({
...node,
expanded: !node.expanded
}),
getNodeKey: ({ node }) => node.id
});
setTreeData(updated);
};
Expand/Collapse All
Toggle expansion state for all nodes:
import { toggleExpandedForAll } from 'react-apple-tree';
// Expand all
const allExpanded = toggleExpandedForAll({
treeData: currentTree,
expanded: true
});
// Collapse all
const allCollapsed = toggleExpandedForAll({
treeData: currentTree,
expanded: false
});
setTreeData(allExpanded);
Respond to Expansion Changes
Use the onVisibilityToggle callback:
<ReactAppleTree
treeData={data}
onChange={setData}
getNodeKey={({ node }) => node.id}
onVisibilityToggle={({ node, expanded, treeData }) => {
console.log(`${node.title} is now ${expanded ? 'expanded' : 'collapsed'}`);
// Optional: Save expansion state to backend
saveExpansionState(node.id, expanded);
}}
/>
Traversing the Tree
Walk (Iterate)
Execute a callback for each node:
import { walk } from 'react-apple-tree';
walk({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node, path, treeIndex }) => {
console.log(`${node.title} at path:`, path);
// Return false to stop traversal
if (node.id === 'target') {
return false;
}
},
ignoreCollapsed: true // Only visit expanded nodes
});
Transform the entire tree:
import { map } from 'react-apple-tree';
const prefixedTree = map({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => ({
...node,
title: `[Prefix] ${node.title}`
}),
ignoreCollapsed: false // Transform all nodes
});
Depth-First Search
Custom DFS traversal with enter/exit callbacks:
src/utils/tree-traversal.ts
import { dfs } from 'react-apple-tree';
dfs({
treeData,
callback: (node) => {
console.log('Visiting:', node.title);
},
onNodeEnter: (node) => {
console.log('Entering:', node.title);
},
onNodeExit: (node) => {
console.log('Exiting:', node.title);
},
ignoreCollapsed: false
});
Utility Functions
Get Descendant Count
Count all descendants of a node:
import { getDescendantCount } from 'react-apple-tree';
const count = getDescendantCount({
node: myNode,
ignoreCollapsed: true // Only count visible descendants
});
console.log(`${count} descendants`);
Get Visible Node Count
Count total visible nodes in the tree:
import { getVisibleNodeCount } from 'react-apple-tree';
const visibleCount = getVisibleNodeCount({ treeData });
console.log(`${visibleCount} visible nodes`);
Get Node at Index
Get node information by tree index:
import { getVisibleNodeInfoAtIndex } from 'react-apple-tree';
const nodeInfo = getVisibleNodeInfoAtIndex({
treeData,
index: 5,
getNodeKey: ({ node }) => node.id
});
if (nodeInfo) {
console.log('Node at index 5:', nodeInfo.node.title);
}
Get Depth
Calculate maximum depth of a node’s descendants:
import { getDepth } from 'react-apple-tree';
const maxDepth = getDepth(rootNode);
console.log(`Tree depth: ${maxDepth}`);
Check Descendant
Check if one node is a descendant of another:
import { isDescendant } from 'react-apple-tree';
if (isDescendant(parentNode, childNode)) {
console.log('childNode is a descendant of parentNode');
}
Common Patterns
Bulk Operations
Perform multiple updates efficiently:
import { map } from 'react-apple-tree';
// Mark multiple nodes as selected
const selectedIds = new Set(['1', '3', '5']);
const updated = map({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => ({
...node,
selected: selectedIds.has(node.id)
}),
ignoreCollapsed: false
});
Search and Replace
Find and update nodes matching criteria:
import { map } from 'react-apple-tree';
const updated = map({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => {
if (node.title?.includes('OLD')) {
return {
...node,
title: node.title.replace('OLD', 'NEW')
};
}
return node;
},
ignoreCollapsed: false
});
Collect All Nodes
Gather all nodes into a flat array:
import { walk } from 'react-apple-tree';
const allNodes: TreeItem[] = [];
walk({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => {
allNodes.push(node);
},
ignoreCollapsed: false
});
console.log(`Total nodes: ${allNodes.length}`);
Find by Predicate
Find nodes matching a condition:
import { walk } from 'react-apple-tree';
let foundNode: TreeItem | null = null;
walk({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => {
if (node.title === 'Target') {
foundNode = node;
return false; // Stop searching
}
},
ignoreCollapsed: false
});
Instead of multiple changeNodeAtPath calls, use map to update multiple nodes in one pass:// Good: Single pass
const updated = map({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => updateLogic(node)
});
// Bad: Multiple passes
let tree = changeNodeAtPath(...);
tree = changeNodeAtPath(...);
tree = changeNodeAtPath(...);
Use ignoreCollapsed wisely
Set ignoreCollapsed: true to skip hidden nodes for better performance:walk({
treeData,
getNodeKey: ({ node }) => node.id,
callback: ({ node }) => { /* ... */ },
ignoreCollapsed: true // Skip collapsed nodes
});
Cache getNodeKey function
Define getNodeKey outside render to avoid recreating it:// Good: Defined once
const getNodeKey = ({ node }) => node.id;
function MyTree() {
return (
<ReactAppleTree
getNodeKey={getNodeKey}
// ...
/>
);
}
// Bad: New function every render
function MyTree() {
return (
<ReactAppleTree
getNodeKey={({ node }) => node.id}
// ...
/>
);
}
Next Steps
Drag and Drop
Learn about drag-and-drop functionality
API Reference
Complete API documentation for all functions