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

@livetree/react

v0.1.0

Published

A powerful and synchronized tree component for React with real‑time collaboration support

Readme

🌳 LiveTree – React Tree Component

Bundle Size + Gzip Size Dependencies

A feature-rich, highly customizable tree component for React with built-in support for collaborative editing, drag & drop, context menus, action buttons, virtual scrolling, RTL, theming, and real‑time synchronization with backend APIs.

✨ Features

✅ Dynamic loading – lazy load child nodes on expand

✅ Multiple root modes – single‑root (with optional root hiding) or multi‑root

✅ Drag & drop – reorder nodes, move between parents, drop before/after/inside

✅ Context menu – custom actions per node

✅ Action buttons – inline buttons (Add, Edit, Delete, etc.) with overflow menu

✅ Virtual scrolling – efficient rendering of large trees

✅ Real‑time sync – integrate with WebSocket updates (nodes:change)

✅ Theming – CSS variables or full class name overrides

✅ RTL support – right‑to‑left layout

✅ Size variants – small, medium, large

✅ Expand/collapse strategies – click on item or on dedicated icon

✅ Show/hide lines – tree connection lines

✅ TypeScript – fully typed

📦 Installation

npm install @livetree/react
# or
yarn add @livetree/react

Note: The component is written in TypeScript and requires React 16.8+ (hooks).

🚀 Quick Start

import LiveTree, { useTree, NodeRow } from '@livetree/react';
import '@livetree/react/style.css'; // optional default styles

function App() {
  const { store, loading } = useTree({
    fetchApi: async (id) => {
      const res = await fetch(`/api/category/${id}`);
      return res.json();
    },
    fetchRootApi: async () => {
      const res = await fetch('/api/category/root');
      return res.json();
    },
  });

  if (loading) return <div>Loading...</div>;

  return (
    <LiveTree
      visibleNodes={store.visibleNodes}
      expandNodesIds={store.expandNodesIds}
      expandHandler={store.expandHandler}
      unExpandHandler={store.unExpandHandler}
      selectNode={store.selectNode}
      selectedItem={store.selectedItem}
    />
  );
}

LiveTree Component Props

|Prop | Type | Default | Description | |:---:|:----:|:--------:|:-----------:| |visibleNodes | NodeRow[] | required | Flattened array of nodes to display (from useTree) | |expandNodesIds | Set | required | Set of expanded node IDs | |expandHandler | (node: NodeRow) => Promise | required | Called when a node should expand | |unExpandHandler | (node: NodeRow) => void | required | Called when a node should collapse | |selectNode | (node: NodeRow | null) => void | required | Callback to set selected node | |selectedItem | NodeRow | null | required | Currently selected node | |actionButtons |ActionButtonsType | undefined | Inline buttons configuration | |rootDisplay |RootDisplay | { mode: 'single-root', showRoot: true } | Root visibility settings | |dir |'ltr' / 'rtl' | 'ltr' | Text direction | |size |'small' / 'medium' / 'large' | 'medium' | Component size | |theme |TreeTheme | {} | CSS class names for styling | |classNames |ClassNames | {} | Additional class names (overrides) | |renderNode |(props: RenderNodeProps) => React.ReactNode | undefined | Custom node renderer | |renderNodeIcon |(node: NodeRow) => React.ReactNode | undefined | Custom icon renderer | |showLines |boolean | true | Show tree guide lines | |constLabelWidth | number | 120 | Fixed label width (px) | |expandStrategy |'click on item' / 'click on expanedIcon' | 'click on item' | How to expand/collapse | |showExpandIcons | boolean | true | Show expand/collapse | buttons | |icons |{ expand?: { expanedIcon, unExpandedIcon }, nodesIcon?: ReactNode } | undefined | Custom expand icons | |dragDrop |{ enabled: boolean; onDrag?, onDrop? } | { enabled: false } | Drag & drop configuration | |virtualization |{ enabled: boolean; rowHeight?, containerHeight?, overscan? } | { enabled: false } | Virtual scrolling settings |

useTree Hook

The useTree hook manages data fetching, flattening, expansion state, and real‑time synchronization.

const {
  store,                // { visibleNodes, expandNodesIds, expandHandler, unExpandHandler, selectedItem, selectNode }
  loading,              // boolean
  fetchedIds,           // Set<string>
  syncNodesWithChange,  // (data: NodesChange<T>) => void
  resetTree,            // () => Promise<void>
  // ... other internals
} = useTree({
  fetchApi,      // (id: string) => Promise<TreeNode<T>>
  fetchRootApi,  // () => Promise<TreeNode<T> | TreeNode<T>[]>
  rootDisplay,   // optional
});

Parameters

|Param | Type | Description | |:---:|:----:|:--------:| |fetchApi | (id: string) => Promise<TreeNode> | Fetch a single node by ID (including its children) | |fetchRootApi | () => Promise<TreeNode TreeNode[]> | Fetch the root node(s) | |rootDisplay | RootDisplay | Same as LiveTree prop |

Return Values

|Property | Type | Description | |:---:|:----:|:--------:| |store | object | Contains visibleNodes, expandNodesIds, handlers, etc. – pass directly to | |loading | boolean | Initial load status | |fetchedIds | Set | IDs of nodes whose children have been fetched | |syncNodesWithChange | (data: NodesChange) => void | Apply WebSocket changes to local state | |resetTree | () => Promise | Reload entire tree from API |

Synchronization with Backend (WebSocket)

When using the backend with nodes:change messages (see Live Tree Backend), call syncNodesWithChange on each WebSocket message:

useEffect(() => {
  const ws = new WebSocket('ws://localhost:3001?userId=xxx&userName=xxx');
  ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    if (message.type === 'nodes:change') {
      syncNodesWithChange(message.payload);
    }
  };
  return () => ws.close();
}, [syncNodesWithChange]);

The syncNodes utility (internal) will:

Add/update/delete nodes in the local Map

Adjust path and depth for moved nodes

Clean up expansion and fetched IDs for deleted nodes

Action Buttons Configuration

const actionButtons = {
  add: {
    Icon: <PlusIcon />,
    title: 'Add child',
    handler: (id, node) => console.log('Add', id),
  },
  edit: {
    Icon: <EditIcon />,
    title: 'Edit',
    showInMenu: true,   // moves to "more" menu
    handler: (id, node) => console.log('Edit', id),
  },
  remove: {
    Icon: <TrashIcon />,
    title: 'Delete',
    handler: async (id, node) => { /* call API */ },
  },
};

Built‑in keys: 'add', 'edit', 'remove', 'removeChildren', 'removeNodeAndChildren'. You can also use any custom string key.

Drag & Drop

<LiveTree
  dragDrop={{
    enabled: true,
    onDrag: (dragNode) => console.log('Dragging', dragNode),
    onDrop: async (dragNode, targetNode, position) => {
      // position: 'before' | 'after' | 'inside'
      await fetch(`/api/category/${dragNode.id}/move`, {
        method: 'PUT',
        body: JSON.stringify({ newParentId: targetNode.id, position }),
      });
    },
  }}
/>

Virtual Scrolling

For trees with thousands of nodes:

<LiveTree
  virtualization={{
    enabled: true,
    rowHeight: 36,           // px
    containerHeight: 600,    // px
    overscan: 5,             // extra rows above/below
  }}
/>

Data Structures

LiveTree expects and uses the following TypeScript types. All types are generic () to allow custom extra data.

1. TreeNode – Server/API Response Format

This is the format your API must return when fetching nodes (e.g., from fetchApi or fetchRootApi).

type TreeNode<T = unknown> = {
  id: string;                    // unique identifier
  label: string;                 // display text
  hasChild: boolean;             // does it have children? (for lazy loading)
  parent: string | null;         // parent ID (null for root)
  isRoot: boolean;               // is this a root node?
  version?: number;              // optional – used for optimistic locking
  icon?: string | React.ReactNode; // custom icon
  createdAt?: Date | string;     // optional metadata
  extraData?: T;                 // any additional data you want to attach
  
  children: Array<{              // IMPORTANT: must be an array of the same structure
    id: string;
    label: string;
    hasChild: boolean;
    version?: number;
    icon?: string | React.ReactNode;
    isRoot: boolean;
    createdAt?: Date | string;
    extraData?: T;
  }>;
};

Example response from /api/category/root:

{
  "id": "root-1",
  "label": "Categories",
  "hasChild": true,
  "parent": null,
  "isRoot": true,
  "version": 5,
  "children": [
    {
      "id": "cat-1",
      "label": "Electronics",
      "hasChild": true,
      "parent": "root-1",
      "isRoot": false,
      "children": []
    }
  ]
}

2. NodesChangeData – WebSocket Update Payload

When you receive a nodes:change message from the server (WebSocket), the data field inside created, updated, or deleted must follow this structure:

type NodesChangeData<T = unknown> = {
  id: string;
  label: string;
  hasChild: boolean;
  parent: string | null;
  isRoot: boolean;
  version?: number;
  icon?: string | React.ReactNode;
  createdAt?: Date | string;
  extraData?: T;
};

The full payload sent to syncNodesWithChange follows the NodesChange<T> interface:

export interface NodesChange<T = unknown> {
  operationId?: string;
  operationType?: string;
  timestamp?: Date;
  changes: {
    created: Array<{ id: string; name: string; data: NodesChangeData<T> }>;
    updated: Array<{ id: string; name: string; changes?: Record<string, NodesChangeData<T>>; data: NodesChangeData<T> }>;
    deleted: Array<{ id: string; name: string; data: NodesChangeData<T> }>;
  };
  metadata?: any;
}

Example WebSocket message:

{
  "type": "nodes:change",
  "payload": {
    "changes": {
      "created": [
        {
          "id": "new-123",
          "name": "New Category",
          "data": {
            "id": "new-123",
            "label": "New Category",
            "hasChild": false,
            "parent": "cat-1",
            "isRoot": false
          }
        }
      ],
      "updated": [],
      "deleted": []
    }
  }
}

3. NodeRow – Internal Flat Node (used by LiveTree component)

You don't need to construct this manually. It is generated by useTree and passed to visibleNodes and other props.

type NodeRow<T = unknown> = {
  id: string;
  label: string;
  depth: number;          // level in tree (1 = root)
  hasChild: boolean;
  parent: string | null;
  path: string;           // e.g., "root-1/cat-1/sub-1"
  version?: number;
  order: number;          // index among siblings
  icon?: string | React.ReactNode;
  isRoot: boolean;
  createdAt?: Date | string;
  extraData?: T;
};

Important Notes:

For real‑time sync, the WebSocket nodes:change payload must exactly match the NodesChangeData structure inside data field.

version field is optional but highly recommended if you implement conflict resolution or optimistic locking.

Theming & Styling

CSS Variables

Override the default variables (see style.css in source):

:root {
  --tree-primary: #dbeafe;
  --tree-primary-hover: #60a5fa;
  --tree-neutral-hover: #e5e7eb;
  --tree-border: #e5e7eb;
  --tree-border-dashed: #3f3f46;
  --tree-text: #1f2937;
  /* ... */
}

Class Name Overrides

Use theme (static class names) or classNames (dynamic functions) props:

<LiveTree
  theme={{
    container: 'my-tree-container',
    node: 'custom-node',
    button: 'custom-toggle',
    label: 'custom-label',
  }}
  classNames={{
    node: (node) => node.isRoot ? 'root-node' : 'child-node',
  }}
/>

Size Variants

The component includes three built‑in sizes: small, medium, large. Use the size prop.

TypeScript Types

The library exports all types:

import {
  LiveTree,
  useTree,
  NodeRow,
  TreeNode,
  NodesChange,
  ActionButtonsType,
  TreeTheme,
  ClassNames,
  RootDisplay,
  Direction,
  Size,
  ExpandStrategy,
  DropPosition,
} from '@livetree/react';

Examples

Basic Tree with Custom Node Renderer

<LiveTree
  {...store}
  renderNode={({ node, isExpanded, onExpand, onCollapse, hasChild, isSelected, selectNode }) => (
    <div style={{ background: isSelected ? '#eef' : 'transparent' }}>
      <span onClick={hasChild ? (isExpanded ? onCollapse : onExpand) : undefined}>
        {node.label}
      </span>
    </div>
  )}
/>

Multi‑root Mode Experimental

useTree({
  fetchRootApi: async () => {
    const res = await fetch('/api/category/roots'); // returns array
    return res.json();
  },
  rootDisplay: { mode: 'multi-root' },
});

Hide Root Node (Single‑root) Experimental

rootDisplay={{
  mode: 'single-root',
  showRoot: false,   // root not displayed, children become top-level
  label: 'Custom Root Label', // optional
}}

📄 License

MIT © Mohammad Karimi (https://github.com/pmka99)

📞 Contact

For questions or suggestions, please open an issue on GitHub or contact the maintainer.

GitHub

Email

🤝 Contributing

Issues and pull requests are welcome. Please follow the existing code style and add tests for new features.

Related Projects

Live Tree Backend (https://github.com/pmka99/livetree-node-server-example) – Node.js + MongoDB + WebSocket server that pairs perfectly with this component.

Happy tree building! 🌳