A flexible, headless React tree component library that provides powerful tree state management with render props pattern.
- 🎯 Headless Design - Complete control over rendering and styling
- 🔧 Flexible API - Use
Tree,VirtualizedTreecomponents or build custom solutions withuseTreeState,flattenTree - 📦 TypeScript Support - Fully typed for better developer experience
- 🚀 Performance - Efficient tree flattening and state management
- ⚡ Virtualization - Handle massive datasets (300,000+ items) with smooth scrolling performance
- 🎨 Render Props - Flexible rendering with full access to tree state
- 🌳 Deep Nesting - Support for deeply nested tree structures (10+ levels)
- ✏️ Tree Manipulation - Insert, remove, and move tree items dynamically with built-in validation
npm install @kryoonminsang/headless-treeyarn add @kryoonminsang/headless-treepnpm add @kryoonminsang/headless-treeimport { Tree } from '@kryoonminsang/headless-tree';
<Tree
initialTree={treeData}
renderItem={({ item, depth, toggleOpenState }) => (
<div style={{ paddingLeft: depth * 20 }}>
<button onClick={toggleOpenState}>{item.isOpened ? '📂' : '📁'}</button>
{item.customData.name}
</div>
)}
/>;import { VirtualizedTree } from '@kryoonminsang/headless-tree';
<VirtualizedTree
initialTree={largeTreeData}
height="400px"
estimateSize={() => 32}
renderItem={({ item, depth, toggleOpenState }) => (
<div style={{ paddingLeft: depth * 16, height: 32 }}>
<button onClick={toggleOpenState}>{item.isOpened ? '📂' : '📁'}</button>
{item.customData.name}
</div>
)}
/>;import { useRef } from 'react';
import { Tree, TreeRef } from '@kryoonminsang/headless-tree';
function ControlledTree() {
const treeRef = useRef<TreeRef>(null);
return (
<>
<button onClick={() => treeRef.current?.openAll()}>
Expand All
</button>
<Tree ref={treeRef} initialTree={treeData} renderItem={...} />
</>
);
}import { useRef } from 'react';
import { Tree, TreeRef } from '@kryoonminsang/headless-tree';
function EditableTree() {
const treeRef = useRef<TreeRef>(null);
const handleAddItem = () => {
const newItem = {
id: 'new-id',
children: [],
customData: { name: 'New Item', type: 'file' },
};
// Insert at the end of root level
treeRef.current?.insertItem(null, newItem, 'last');
};
const handleRemoveItem = (itemId: string) => {
treeRef.current?.removeItem(itemId);
};
const handleMoveItem = (sourceId: string, targetParentId: string | null) => {
treeRef.current?.moveItem(sourceId, {
parentId: targetParentId,
position: 'last',
});
};
return (
<>
<button onClick={handleAddItem}>Add Item</button>
<Tree
ref={treeRef}
initialTree={treeData}
renderItem={({ item }) => (
<div>
{item.customData.name}
<button onClick={() => handleRemoveItem(item.id)}>Delete</button>
</div>
)}
/>
</>
);
}interface FileItem {
name: string;
type: 'file' | 'folder';
}
const treeData = {
rootIds: ['1', '2'],
items: {
'1': {
id: '1',
children: ['1-1'],
customData: { name: 'src', type: 'folder' },
},
'1-1': {
id: '1-1',
children: [],
customData: { name: 'index.ts', type: 'file' },
},
'2': {
id: '2',
children: [],
customData: { name: 'package.json', type: 'file' },
},
},
};Basic tree component for standard use cases.
<Tree
initialTree={treeData}
options={{
syncWithInitialTree?: boolean
initialOpenedIds?: TreeItemId[]
}}
renderItem={(params: RenderItemParams) => ReactNode}
/>Virtualized tree component for large datasets.
<VirtualizedTree
initialTree={treeData}
height={number | string}
estimateSize={(index: number) => number}
overscan={number}
options={{
syncWithInitialTree?: boolean
initialOpenedIds?: TreeItemId[]
}}
renderItem={(params: RenderItemParams) => ReactNode}
// ... any div props (className, style, etc.)
/>Props:
height- Height of the virtualized containerestimateSize- Function returning estimated height of each itemoverscan- Number of items to render outside visible area (default: 5)- All standard HTML div props are supported
Both Tree and VirtualizedTree accept an options prop with the following configuration:
If true, synchronizes the tree with initialTree whenever initialTree changes. Useful when tree data needs to be modified from outside the tree component. However, memoization handling such as useState, useMemo, select, etc. is required for use.
<Tree
initialTree={treeData}
options={{ syncWithInitialTree: true }}
renderItem={...}
/>Initial set of opened item IDs. If provided, this takes precedence over isOpened flags in tree items. This is useful for separating tree structure data from UI state.
<Tree
initialTree={treeData}
options={{ initialOpenedIds: ['1', '2', '3'] }}
renderItem={...}
/>const {
tree,
parentMap,
childrenIndexMap,
open,
close,
toggleOpen,
openAll,
closeAll,
insertItem,
removeItem,
moveItem,
} = useTreeState({ initialTree, options });State Management:
tree- Current tree data with merged open/close stateparentMap- Map for O(1) parent lookup performancechildrenIndexMap- Map for tracking child positions
Open/Close Operations:
open(id)- Open a specific tree itemclose(id)- Close a specific tree itemtoggleOpen(id)- Toggle open/close state of an itemopenAll()- Expand all tree itemscloseAll()- Collapse all tree items
Tree Manipulation:
insertItem(parentId, newItem, position)- Insert a new item into the treeremoveItem(itemId)- Remove an item and all its descendantsmoveItem(sourceId, target)- Move an item to a new position
const flattenedItems = flattenTree(tree);
// Returns: Array<{ item, depth, parentId, isLastTreeInSameDepth, completeDepthHashTable }>Flattens the tree structure into a linear array for rendering.
const isValid = canMoveItem(tree, sourceId, targetId);Validates whether a tree item can be moved to a target position. Returns false if:
- Source and target are the same item
- Target is a descendant of source (prevents circular references)
const descendantIds = getAllDescendantIds(tree, itemId);Returns an array of all descendant IDs for a given item.
const path = getPath(tree, itemId);Returns the path from root to the specified item as an array of IDs.
const treeRef = useRef<TreeRef<YourDataType>>(null);Provides access to:
tree- Current tree dataparentMap- Parent lookup mapchildrenIndexMap- Children index mapopen(id)- Open an itemclose(id)- Close an itemtoggleOpen(id)- Toggle an item's stateopenAll()- Expand all itemscloseAll()- Collapse all itemsinsertItem(parentId, newItem, position)- Insert a new itemremoveItem(itemId)- Remove an itemmoveItem(sourceId, target)- Move an item
const treeRef = useRef<VirtualizedTreeRef<YourDataType>>(null);Includes all TreeRef methods plus:
virtualizer- Access to the underlying virtualizer instance
The insertItem function supports multiple position types:
// Insert at specific index (0-based)
insertItem(parentId, newItem, 0); // Insert as first child
insertItem(parentId, newItem, 2); // Insert at index 2
// Insert at predefined positions
insertItem(parentId, newItem, 'first'); // Insert at the beginning
insertItem(parentId, newItem, 'last'); // Insert at the end
// Insert relative to existing items
insertItem(parentId, newItem, { before: 'item-id' }); // Insert before an item
insertItem(parentId, newItem, { after: 'item-id' }); // Insert after an item
// Insert at root level (parentId = null)
insertItem(null, newItem, 'last'); // Add to root levelThe moveItem function allows repositioning items within the tree:
// Move to specific index
moveItem(sourceId, { parentId: 'target-parent', index: 0 });
// Move to first or last position
moveItem(sourceId, { parentId: 'target-parent', position: 'first' });
moveItem(sourceId, { parentId: 'target-parent', position: 'last' });
// Move to root level
moveItem(sourceId, { parentId: null, position: 'last' });Validation: Always validate moves using canMoveItem to prevent circular references:
import { canMoveItem } from '@kryoonminsang/headless-tree';
if (canMoveItem(tree, sourceId, targetId)) {
moveItem(sourceId, { parentId: targetId, position: 'last' });
} else {
console.error('Cannot move item to its own descendant');
}The removeItem function removes an item and all its descendants:
// Removes the item and all children recursively
removeItem('item-id');This library is fully typed with TypeScript. All types are exported and can be imported:
import type {
TreeData,
BasicTreeItem,
RenderItemParams,
TreeItemId,
TreeRef,
VirtualizedTreeProps,
VirtualizedTreeRef,
ParentMap,
ChildrenIndexMap,
} from '@kryoonminsang/headless-tree';We welcome contributions! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Install dependencies
pnpm install
# Run development server
pnpm dev
# Run tests
pnpm test
# Run linter
pnpm lint
# Build library
pnpm buildThis project is licensed under the MIT License - see the LICENSE file for details.
If you encounter any issues or have feature requests, please create an issue on GitHub Issues.