@kryoonminsang/headless-tree
v0.0.11
Published
Headless tree component for React
Maintainers
Readme
headless-tree
A flexible, headless React tree component library that provides powerful tree state management with render props pattern.
Features
- 🎯 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
Installation
npm install @kryoonminsang/headless-treeyarn add @kryoonminsang/headless-treepnpm add @kryoonminsang/headless-treeQuick Start
Basic Tree
import { 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>
)}
/>;Virtualized Tree (for large datasets)
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>
)}
/>;With Controls
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={...} />
</>
);
}With Tree Manipulation
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>
)}
/>
</>
);
}Data Structure
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' },
},
},
};API Reference
Components
Tree
Basic tree component for standard use cases.
<Tree
initialTree={treeData}
options={{ syncWithInitialTree?: boolean }}
renderItem={(params: RenderItemParams) => ReactNode}
/>VirtualizedTree
Virtualized tree component for large datasets.
<VirtualizedTree
initialTree={treeData}
height={number | string}
estimateSize={(index: number) => number}
overscan={number}
options={{ syncWithInitialTree?: boolean }}
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
Hooks
useTreeState
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
Utilities
flattenTree
const flattenedItems = flattenTree(tree);
// Returns: Array<{ item, depth, parentId, isLastTreeInSameDepth, completeDepthHashTable }>Flattens the tree structure into a linear array for rendering.
canMoveItem
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)
getAllDescendantIds
const descendantIds = getAllDescendantIds(tree, itemId);Returns an array of all descendant IDs for a given item.
getPath
const path = getPath(tree, itemId);Returns the path from root to the specified item as an array of IDs.
Refs
TreeRef
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
VirtualizedTreeRef
const treeRef = useRef<VirtualizedTreeRef<YourDataType>>(null);Includes all TreeRef methods plus:
virtualizer- Access to the underlying virtualizer instance
Advanced Usage
Insert Operations
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 levelMove Operations
The 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');
}Remove Operations
The removeItem function removes an item and all its descendants:
// Removes the item and all children recursively
removeItem('item-id');TypeScript Support
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';Contributing
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
Development
# Install dependencies
pnpm install
# Run development server
pnpm dev
# Run tests
pnpm test
# Run linter
pnpm lint
# Build library
pnpm buildLicense
This project is licensed under the MIT License - see the LICENSE file for details.
Issues
If you encounter any issues or have feature requests, please create an issue on GitHub Issues.
