npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-dendro

v0.1.0

Published

High-performance React Tree component with virtualization and drag-and-drop support

Readme

react-dendro

npm version npm downloads bundle size license CI

High-performance React Tree component with virtualization and drag-and-drop support.

Features

  • Virtualization — Efficiently renders large trees with thousands of nodes
  • Drag & Drop — Built-in support for reordering and moving nodes (including cross-tree)
  • TypeScript — Full type safety with generics support
  • Customizable — Render props for complete control over node appearance
  • Accessible — Keyboard navigation support
  • Zustand Store — External state management for data, selection, focus, and open states
  • Global API — Access tree methods from anywhere via getTreeApi()

Installation

npm install react-dendro
# or
yarn add react-dendro
# or
pnpm add react-dendro

Table of Contents

Quick Start

import { Tree, TreeStoreProvider } from 'react-dendro'

interface FileNode {
  id: string
  name: string
  children?: FileNode[]
}

const data: FileNode[] = [
  {
    id: '1',
    name: 'Documents',
    children: [
      { id: '2', name: 'report.pdf' },
      { id: '3', name: 'notes.txt' },
    ],
  },
]

function App() {
  return (
    <TreeStoreProvider names={['file-tree']}>
      <Tree<FileNode>
        name="file-tree"
        width="100%"
        height={400}
      >
        {({ node, style }) => (
          <div style={style}>
            {node.isInternal && (node.isOpen ? '▼ ' : '▶ ')}
            {node.data.name}
          </div>
        )}
      </Tree>
    </TreeStoreProvider>
  )
}

Architecture

react-dendro uses a Zustand-based architecture where tree state is managed externally:

┌────────────────────────────────────────────┐
│              TreeStoreProvider             │
│  (Creates Zustand stores for each tree)    │
│                                            │
│  ┌─────────────────┐  ┌─────────────────┐  │
│  │   Tree Store    │  │   Tree Store    │  │
│  │  "devices-tree" │  │  "users-tree"   │  │
│  │                 │  │                 │  │
│  │ - data          │  │ - data          │  │
│  │ - selection     │  │ - selection     │  │
│  │ - focus         │  │ - focus         │  │
│  │ - open states   │  │ - open states   │  │
│  └────────┬────────┘  └────────┬────────┘  │
│           │                    │           │
│  ┌────────▼────────┐  ┌────────▼────────┐  │
│  │  Tree Component │  │  Tree Component │  │
│  │  name="devices" │  │  name="users"   │  │
│  └─────────────────┘  └─────────────────┘  │
└────────────────────────────────────────────┘

TreeStoreProvider

Wrap your application (or the part that uses trees) with TreeStoreProvider:

import { TreeStoreProvider } from 'react-dendro'

// Define tree names as constants for type safety
const TREE_NAMES = {
  DEVICES: 'devices-tree',
  USERS: 'users-tree',
} as const

function App() {
  return (
    <TreeStoreProvider names={Object.values(TREE_NAMES)}>
      {/* Your app components */}
    </TreeStoreProvider>
  )
}

Tree Component

Basic Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | name | string | required | Unique tree identifier | | loading | boolean | false | Shows loading indicator | | width | number \| string | "100%" | Tree width | | height | number | - | Tree height in pixels | | rowHeight | number | 32 | Row height in pixels | | indent | number | 20 | Indentation per level | | overscanCount | number | 5 | Extra rows for smooth scrolling | | openByDefault | boolean | false | Expand all nodes initially | | className | string | - | CSS class for tree container | | rowClassName | string | - | CSS class for rows |

Data Accessors

| Prop | Type | Default | Description | |------|------|---------|-------------| | idAccessor | string \| (d: T) => string | "id" | Get node ID | | childrenAccessor | string \| (d: T) => T[] \| null | "children" | Get children | | sortAccessor | string \| (d: T) => string \| number | - | Sort key for insertions |

Event Handlers

| Prop | Type | Description | |------|------|-------------| | onActivate | (node: NodeApi<T>) => void | Node double-click/Enter | | onSelect | (nodes: NodeApi<T>[]) => void | Selection change | | onFocus | (node: NodeApi<T>) => void | Node focus change | | onToggle | (id: string) => void | Node expand/collapse | | onMove | MoveHandler<T> | Node moved via drag & drop | | onCreate | CreateHandler<T> | New node created | | onDelete | DeleteHandler<T> | Node deleted | | onScroll | (event: UIEvent) => void | Tree scroll | | onContextMenu | MouseEventHandler | Right-click on tree |

Renderers

| Prop | Type | Description | |------|------|-------------| | children | ElementType<NodeRendererProps<T>> | Node content renderer | | renderRow | ElementType<RowRendererProps<T>> | Row wrapper renderer | | renderDragPreview | ElementType<DragPreviewProps> | Drag preview renderer | | renderCursor | ElementType<CursorProps> | Drop cursor renderer | | renderContainer | ElementType | Tree container wrapper | | renderLoader | ElementType | Loading indicator |

Working with Data

Setting Initial Data

Data is set via the tree store, not props. This allows external updates:

import { useTreeStore } from 'react-dendro'
import { useEffect } from 'react'

function DeviceTreePanel() {
  const { data: treeData, isLoading } = useQuery(['devices'], fetchDevices)
  
  // Get the store for this tree
  const deviceStore = useTreeStore('devices-tree')
  const setData = deviceStore(state => state.setData)
  
  // Update tree data when it changes
  useEffect(() => {
    if (treeData) {
      setData(treeData)
    }
  }, [treeData, setData])
  
  return (
    <Tree name="devices-tree" loading={isLoading} height={500}>
      {/* ... */}
    </Tree>
  )
}

CRUD Operations

The store provides methods for manipulating data:

const store = useTreeStore('devices-tree')

// Insert a single node
const insertNode = store(state => state.insertNode)
insertNode('parent-id', { id: 'new-1', name: 'New Node' })

// Insert multiple nodes
const insertNodes = store(state => state.insertNodes)
insertNodes([
  { parentId: 'parent-1', node: { id: 'new-1', name: 'Node 1' } },
  { parentId: 'parent-2', node: { id: 'new-2', name: 'Node 2' } },
])

// Update a node
const updateNode = store(state => state.updateNode)
updateNode('node-id', { name: 'Updated Name' })

// Update multiple nodes
const updateNodes = store(state => state.updateNodes)
updateNodes([
  { id: 'node-1', patch: { name: 'Name 1' } },
  { id: 'node-2', patch: { name: 'Name 2' } },
])

// Delete a node
const deleteNode = store(state => state.deleteNode)
deleteNode('node-id')

// Delete multiple nodes
const deleteNodes = store(state => state.deleteNodes)
deleteNodes(['node-1', 'node-2', 'node-3'])

// Get node by ID
const getNodeById = store(state => state.getNodeById)
const node = getNodeById('node-id')

Opening Nodes Programmatically

const store = useTreeStore('devices-tree')
const openNode = store(state => state.nodes_open__open)

// Open a specific node
openNode('node-id', false) // false = not in filtered mode

Node Rendering

The children prop receives render props for each node:

type NodeRendererProps<T> = {
  style: CSSProperties       // Positioning styles with indentation
  node: NodeApi<T>           // Node API with data and methods
  tree: TreeApi<T>           // Tree API
  dragHandle?: (el: HTMLDivElement | null) => void  // Drag handle ref
  preview?: boolean          // true if rendered in drag preview
}

Basic Node Renderer

<Tree name="my-tree" height={400}>
  {({ node, style, dragHandle }) => (
    <div ref={dragHandle} style={style}>
      {node.isInternal && (
        <button onClick={() => node.toggle()}>
          {node.isOpen ? '▼' : '▶'}
        </button>
      )}
      <span>{node.data.name}</span>
      {node.isSelected && ' ✓'}
    </div>
  )}
</Tree>

Performance Optimization

For maximum performance with large trees and complex node components, follow these best practices:

1. Create a Memoized TreeItem Component

Extract node rendering into a separate memoized component:

import { memo } from 'react'

interface TreeItemProps {
  // Layout
  rowHeight: number
  style: React.CSSProperties
  dragHandle?: (el: HTMLDivElement | null) => void
  
  // State flags (primitives only!)
  isFocusSwitching: boolean
  isSelectionSwitching: boolean
  isSelected: boolean
  willReceiveDrop: boolean
  isDragging: boolean
  isInDragSelection: boolean
  level: number
  isLeaf: boolean
  isOpen: boolean
  
  // Bound methods
  toggle: () => void
  select: () => void
  selectMulti: () => void
  selectContiguous: () => void
  deselect: () => void
  delete: () => void
  
  // Data (primitives only!)
  id: string
  name: string
  type: string
  // ... other primitive data fields
}

const TreeItem = memo((props: TreeItemProps) => {
  const {
    rowHeight,
    style,
    isFocusSwitching,
    isSelected,
    level,
    isLeaf,
    isOpen,
    toggle,
    select,
    dragHandle,
    id,
    name,
    type,
  } = props

  return (
    <div
      ref={dragHandle}
      style={{ height: rowHeight, ...style }}
      onClick={select}
    >
      {!isLeaf && (
        <button onClick={(e) => { e.stopPropagation(); toggle(); }}>
          {isOpen ? '▼' : '▶'}
        </button>
      )}
      <span style={{ marginLeft: level * 20 }}>{name}</span>
      {isSelected && ' ✓'}
    </div>
  )
}, areEqualTreeItemProps)

// Custom comparison function for optimal re-renders
const areEqualTreeItemProps = (prev: TreeItemProps, next: TreeItemProps) => {
  return (
    prev.rowHeight === next.rowHeight &&
    prev.isFocusSwitching === next.isFocusSwitching &&
    prev.isSelectionSwitching === next.isSelectionSwitching &&
    prev.isSelected === next.isSelected &&
    prev.willReceiveDrop === next.willReceiveDrop &&
    prev.isDragging === next.isDragging &&
    prev.isInDragSelection === next.isInDragSelection &&
    prev.level === next.level &&
    prev.isLeaf === next.isLeaf &&
    prev.isOpen === next.isOpen &&
    prev.id === next.id &&
    prev.name === next.name &&
    prev.type === next.type
  )
}

2. Pass Primitives and Bound Methods

CRITICAL: Pass only primitive values to your TreeItem, and use .bind(node) for methods:

<Tree<MyNode> name="my-tree" height={500}>
  {({ node, style, dragHandle }) => (
    <TreeItem
      rowHeight={node.rowHeight}
      style={style}
      dragHandle={dragHandle}
      
      // State flags - these are primitive booleans
      isFocusSwitching={node.isFocusSwitching}
      isSelectionSwitching={node.isSelectionSwitching}
      isSelected={node.state.isSelected}
      willReceiveDrop={node.willReceiveDrop}
      isDragging={node.isDragging}
      isInDragSelection={node.isInDragSelection}
      level={node.level}
      isLeaf={node.isLeaf}
      isOpen={node.isOpen}
      
      // IMPORTANT: Use .bind(node) for methods!
      // This creates stable function references
      toggle={node.toggle.bind(node)}
      select={node.select.bind(node)}
      selectMulti={node.selectMulti.bind(node)}
      selectContiguous={node.selectContiguous.bind(node)}
      delete={node.delete.bind(node)}
      deselect={node.deselect.bind(node)}
      
      // Data - extract primitives from node.data
      id={node.data.id}
      name={node.data.name}
      type={node.data.type}
    />
  )}
</Tree>

Why .bind(node)?
Without .bind(node), arrow functions like () => node.toggle() would create new function references on every render, defeating memoization.

3. Use isFocusSwitching and isSelectionSwitching

When users rapidly navigate the tree (holding arrow keys) or select many nodes, isFocusSwitching and isSelectionSwitching become true. Use these to simplify rendering during batch operations:

const TreeItem = memo((props: TreeItemProps) => {
  const { isFocusSwitching, isSelectionSwitching, name } = props

  // Skip rendering expensive elements during rapid operations
  if (isFocusSwitching || isSelectionSwitching) {
    return (
      <div style={props.style}>
        <span>{name}</span>
      </div>
    )
  }

  // Full render with all features
  return (
    <div style={props.style}>
      <Checkbox checked={props.isSelected} />
      <ExpensiveIcon type={props.type} />
      <span>{name}</span>
      <Tooltip content={props.description}>
        <InfoIcon />
      </Tooltip>
      <DeleteButton onClick={props.delete} />
    </div>
  )
})

When are these flags true?

  • isFocusSwitching — Active for ~100ms after rapid focus changes (arrow key navigation)
  • isSelectionSwitching — Active for ~500ms after rapid selection changes (Shift+Click ranges)

4. Full Production Example

import { memo, useCallback, useEffect } from 'react'
import { Tree, useTreeStore, type NodeApi } from 'react-dendro'

// Types
interface DeviceNode {
  id: string
  name: string
  type: 'folder' | 'device'
  status?: 'online' | 'offline'
}

interface TreeItemProps {
  rowHeight: number
  style: React.CSSProperties
  dragHandle?: (el: HTMLDivElement | null) => void
  isFocusSwitching: boolean
  isSelected: boolean
  willReceiveDrop: boolean
  isDragging: boolean
  isInDragSelection: boolean
  level: number
  isLeaf: boolean
  isOpen: boolean
  toggle: () => void
  select: () => void
  selectMulti: () => void
  selectContiguous: () => void
  deselect: () => void
  delete: () => void
  id: string
  name: string
  type: string
  status?: string
}

// Memoized comparison
const areEqual = (prev: TreeItemProps, next: TreeItemProps) => (
  prev.rowHeight === next.rowHeight &&
  prev.isFocusSwitching === next.isFocusSwitching &&
  prev.isSelected === next.isSelected &&
  prev.willReceiveDrop === next.willReceiveDrop &&
  prev.isDragging === next.isDragging &&
  prev.isInDragSelection === next.isInDragSelection &&
  prev.level === next.level &&
  prev.isLeaf === next.isLeaf &&
  prev.isOpen === next.isOpen &&
  prev.id === next.id &&
  prev.name === next.name &&
  prev.type === next.type &&
  prev.status === next.status
)

// Memoized TreeItem
const TreeItem = memo((props: TreeItemProps) => {
  const {
    rowHeight, style, dragHandle,
    isFocusSwitching, isSelected, willReceiveDrop,
    isDragging, isInDragSelection,
    level, isLeaf, isOpen,
    toggle, select, selectMulti, selectContiguous,
    id, name, type, status,
  } = props

  const handleClick = (e: React.MouseEvent) => {
    e.stopPropagation()
    if (e.shiftKey) {
      selectContiguous()
    } else if (e.ctrlKey || e.metaKey) {
      isSelected ? props.deselect() : selectMulti()
    } else {
      select()
    }
  }

  const handleDoubleClick = (e: React.MouseEvent) => {
    e.stopPropagation()
    if (!isLeaf) toggle()
  }

  return (
    <div
      ref={dragHandle}
      style={{ height: rowHeight, ...style }}
      onClick={handleClick}
      onDoubleClick={handleDoubleClick}
      className={`
        tree-item
        ${isSelected ? 'selected' : ''}
        ${willReceiveDrop ? 'drop-target' : ''}
        ${isDragging ? 'dragging' : ''}
      `}
    >
      {/* Toggle button */}
      {!isLeaf && (
        <button onClick={(e) => { e.stopPropagation(); toggle(); }}>
          {isOpen ? '▼' : '▶'}
        </button>
      )}

      {/* Checkbox - skip during rapid operations */}
      {!isFocusSwitching && (
        <input
          type="checkbox"
          checked={isSelected}
          onChange={() => isSelected ? props.deselect() : selectMulti()}
          onClick={(e) => e.stopPropagation()}
        />
      )}

      {/* Content */}
      <span className="node-name">{name}</span>

      {/* Status indicator - skip during rapid operations */}
      {!isFocusSwitching && status && (
        <span className={`status ${status}`} />
      )}
    </div>
  )
}, areEqual)

// Main component
function DeviceTree() {
  const { data, isLoading } = useQuery(['devices'], fetchDevices)
  
  const store = useTreeStore('devices-tree')
  const setData = store(state => state.setData)
  
  useEffect(() => {
    if (data) setData(data)
  }, [data, setData])

  const handleDelete = useCallback(async (args: {
    ids: string[]
    nodes: NodeApi<DeviceNode>[]
  }) => {
    await deleteDevices(args.ids)
  }, [])

  return (
    <Tree<DeviceNode>
      name="devices-tree"
      loading={isLoading}
      height={600}
      width="100%"
      rowHeight={32}
      indent={20}
      overscanCount={20}
      onDelete={handleDelete}
      canDragNode={(node) => node.level !== 0}
      canSelectNode={(node) => node.level !== 0}
      className="device-tree"
      rowClassName="device-tree-row"
    >
      {({ node, style, dragHandle }) => (
        <TreeItem
          rowHeight={node.rowHeight}
          style={style}
          dragHandle={dragHandle}
          isFocusSwitching={node.isFocusSwitching}
          isSelected={node.state.isSelected}
          willReceiveDrop={node.willReceiveDrop}
          isDragging={node.isDragging}
          isInDragSelection={node.isInDragSelection}
          level={node.level}
          isLeaf={node.isLeaf}
          isOpen={node.isOpen}
          toggle={node.toggle.bind(node)}
          select={node.select.bind(node)}
          selectMulti={node.selectMulti.bind(node)}
          selectContiguous={node.selectContiguous.bind(node)}
          delete={node.delete.bind(node)}
          deselect={node.deselect.bind(node)}
          id={node.data.id}
          name={node.data.name}
          type={node.data.type}
          status={node.data.status}
        />
      )}
    </Tree>
  )
}

NodeApi

NodeApi provides access to individual node data and methods:

Properties

| Property | Type | Description | |----------|------|-------------| | id | string | Node identifier | | data | T | Node data object | | level | number | Nesting level (0 = root) | | parent | NodeApi<T> \| null | Parent node | | children | NodeApi<T>[] \| null | Child nodes (null if leaf) | | rowIndex | number \| null | Index in visible list | | rowHeight | number | Row height in pixels | | indent | number | Indentation in pixels |

State Properties

| Property | Type | Description | |----------|------|-------------| | isRoot | boolean | Is root node | | isLeaf | boolean | Has no children | | isInternal | boolean | Has children | | isOpen | boolean | Is expanded | | isClosed | boolean | Is collapsed | | isSelected | boolean | Is selected | | isFocused | boolean | Has focus | | isDragging | boolean | Being dragged | | isInDragSelection | boolean | In drag selection | | willReceiveDrop | boolean | Drop target | | isFocusSwitching | boolean | Rapid focus changes happening | | isSelectionSwitching | boolean | Rapid selection changes happening |

Methods

| Method | Description | |--------|-------------| | toggle() | Toggle expand/collapse | | open() | Expand node | | close() | Collapse node | | openParents() | Expand all ancestors | | select() | Select only this node | | selectMulti() | Add to selection | | selectContiguous() | Select range from anchor | | deselect() | Remove from selection | | focus() | Focus this node | | activate() | Trigger onActivate | | delete() | Delete this node | | handleClick(e) | Standard click handler |

Navigation Properties

| Property | Type | Description | |----------|------|-------------| | next | NodeApi<T> \| null | Next visible node | | prev | NodeApi<T> \| null | Previous visible node | | nextSibling | NodeApi<T> \| null | Next sibling | | childIndex | number | Index among siblings |

TreeApi

TreeApi provides programmatic control over the entire tree.

Accessing TreeApi

// Via global registry (from anywhere)
import { getTreeApi } from 'react-dendro'
const api = getTreeApi('devices-tree')
api?.scrollTo('node-123')

// Via ref
const treeRef = useRef<TreeApi<MyNode>>(null)
<Tree ref={treeRef} name="my-tree" ... />
treeRef.current?.openAll()

// Get all tree APIs
import { getAllTreeApis } from 'react-dendro'
const allApis = getAllTreeApis()
allApis.forEach(api => api.closeAll())

Node Access Methods

| Method | Return | Description | |--------|--------|-------------| | get(id) | NodeApi \| null | Get visible node by ID | | findNode(id) | NodeApi \| null | Find any node by ID | | at(index) | NodeApi \| null | Get node by visible index | | indexOf(id) | number \| null | Get visible index | | nodesBetween(id1, id2) | NodeApi[] | Get range of nodes |

Properties

| Property | Type | Description | |----------|------|-------------| | firstNode | NodeApi \| null | First visible node | | lastNode | NodeApi \| null | Last visible node | | focusedNode | NodeApi \| null | Currently focused node | | selectedNodes | NodeApi[] | All selected nodes | | selectedIds | Set<string> | IDs of selected nodes | | visibleNodes | NodeApi[] | All visible nodes | | root | NodeApi | Root node | | isFiltered | boolean | Search is active | | hasFocus | boolean | Tree has focus |

Selection Methods

| Method | Description | |--------|-------------| | select(id) | Select single node | | selectMulti(id) | Add to selection | | selectContiguous(id) | Select range | | selectAll() | Select all visible | | deselect(id) | Remove from selection | | deselectAll() | Clear selection | | setSelection({ ids, anchor, mostRecent }) | Set selection state |

Open/Close Methods

| Method | Description | |--------|-------------| | open(id) | Expand node | | close(id) | Collapse node | | toggle(id) | Toggle expand/collapse | | openParents(id) | Expand ancestors | | openSiblings(node) | Toggle siblings | | openAll() | Expand all nodes | | closeAll() | Collapse all nodes |

Scroll Methods

| Method | Description | |--------|-------------| | scrollTo(id, align?) | Scroll to node (align: 'auto', 'start', 'center', 'end') | | pageUp() | Scroll page up | | pageDown() | Scroll page down |

Other Methods

| Method | Description | |--------|-------------| | focus(id) | Focus node | | activate(id) | Trigger activation | | delete(ids) | Delete nodes | | create(opts) | Create new node | | createLeaf() | Create leaf node | | createInternal() | Create folder node |

Drag & Drop

Configuration

| Prop | Type | Default | Description | |------|------|---------|-------------| | canDragNode | (node) => boolean | true | Can node be dragged | | canDropNode | (args) => boolean | true | Can drop at location | | onlyFolderDrop | boolean | false | Only drop into folders | | disableDropWhenFiltered | boolean | true | Block drops during search | | dndRootElement | Node | - | Root for drag preview |

MoveHandler

type MoveHandler<T> = (args: {
  dragIds: string[]
  dragNodes: NodeApi<T>[]
  parentId: string | null
  parentNode: NodeApi<T> | null
  index: number
  sourceTreeName: string | null  // For cross-tree drops
  targetTreeName: string | null
  targetId: string | null
}) => void | Promise<void>

Cross-Tree Drag & Drop

react-dendro supports dragging nodes between different trees:

<Tree
  name="source-tree"
  onMove={async ({ dragIds, sourceTreeName, targetTreeName, targetId }) => {
    if (sourceTreeName !== targetTreeName) {
      // Cross-tree move
      await createLink({
        sourceTree: sourceTreeName,
        targetTree: targetTreeName,
        sourceIds: dragIds,
        targetId: targetId,
      })
    }
  }}
>
  {/* ... */}
</Tree>

Custom Drag Preview

const DragPreview = ({
  offset,
  isDragging,
  dragIds,
  sourceTreeName,
  targetTreeName,
  targetId,
}: DragPreviewProps) => {
  if (!isDragging) return null
  
  return (
    <div
      style={{
        position: 'fixed',
        left: offset?.x ?? 0,
        top: offset?.y ?? 0,
        pointerEvents: 'none',
      }}
    >
      Moving {dragIds.length} items
    </div>
  )
}

<Tree renderDragPreview={DragPreview} ... />

Search & Filtering

Basic Search

const [searchTerm, setSearchTerm] = useState('')

<Tree
  name="my-tree"
  searchTerm={searchTerm}
  // ... other props
>
  {/* ... */}
</Tree>

<input
  type="text"
  value={searchTerm}
  onChange={(e) => setSearchTerm(e.target.value)}
  placeholder="Search..."
/>

Custom Search Match

<Tree
  searchMatch={(node, term) => {
    // Custom matching logic
    return node.data.name.toLowerCase().includes(term.toLowerCase()) ||
           node.data.tags?.some(tag => tag.includes(term))
  }}
>
  {/* ... */}
</Tree>

API Reference

Exports

// Components
export { Tree } from './components/Tree'

// Providers
export { TreeStoreProvider } from './providers/TreeStoreProvider'

// Hooks
export { useTreeStore } from './hooks/useTreeStore'
export { useTreeStoresMap } from './hooks/useTreeStoresMap'

// APIs
export { NodeApi } from './interfaces/node-api'
export { TreeApi } from './interfaces/tree-api'
export { getTreeApi, getAllTreeApis } from './store/tree-api-registry'

// Types
export type { TreeProps } from './types/tree-props'
export type { NodeState } from './types/state'
export type { 
  NodeRendererProps,
  RowRendererProps,
  DragPreviewProps,
  CursorProps 
} from './types/renderers'
export type { 
  CreateHandler,
  MoveHandler,
  DeleteHandler 
} from './types/handlers'

Store State

The tree store contains the following slices:

| Slice | Purpose | |-------|---------| | DataSlice | Tree data and CRUD operations | | SelectionSlice | Selected nodes | | FocusSlice | Focused node | | OpenSlice | Expanded nodes | | DragSlice | Drag state | | DndSlice | Drop cursor state |

Data Slice Methods

| Method | Description | |--------|-------------| | setData(data) | Set tree data | | insertNode(parentId, node) | Insert single node | | insertNodes(entries) | Insert multiple nodes | | updateNode(id, patch) | Update single node | | updateNodes(entries) | Update multiple nodes | | deleteNode(id) | Delete single node | | deleteNodes(ids) | Delete multiple nodes | | getNodeById(id) | Get node data by ID | | getNodesByIds(ids) | Get multiple nodes |

License

MIT