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

@happyhyep/tree-component

v1.0.6

Published

React Tree Component with Search functionality

Readme

🌳 @happyhyep/tree-component

🇰🇷 한국어 | 🇺🇸 English

🇺🇸 English ver

React Tree Component Library where both folders and files are clickable

npm version License: MIT

✨ Key Differentiators

Unlike other Tree components, this revolutionary Tree component allows both folders and files to be clickable for interaction.

| Feature | Typical Tree | 🌳 Tree Component | | -------------------- | ----------------------------------- | ------------------------------------ | | Folder Click | ❌ Expand/collapse only | ✅ Click event + expand/collapse | | File Click | ✅ Clickable | ✅ Clickable | | Search Feature | ❌ Requires separate implementation | ✅ Built-in search + highlight | | Default Expand State | ❌ Manual setup | ✅ Expand all folders at once | | TypeScript | ⚠️ Limited support | ✅ Full type safety |

🎬 Demo

Tree Component Usage Examples

Tree Component Demo

📦 Installation

# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component

🚀 Quick Start

1️⃣ Basic Tree Component

import React, { useState } from 'react';
import { Tree, TreeItem } from '@happyhyep/tree-component';

interface FileData {
  name: string;
  type: 'folder' | 'file';
  size?: number;
}

const data: TreeItem<FileData>[] = [
  {
    id: '1',
    parentId: null,
    canOpen: true,
    data: { name: 'Documents', type: 'folder' },
  },
  {
    id: '2',
    parentId: '1',
    canOpen: false,
    data: { name: 'report.pdf', type: 'file', size: 1024 },
  },
  {
    id: '3',
    parentId: '1',
    canOpen: true,
    data: { name: 'Projects', type: 'folder' },
  },
];

function MyApp() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <Tree
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => {
        setSelectedId(item.id);
        console.log('Clicked item:', item.data);

        // 💡 Both folders and files are clickable!
        if (item.data.type === 'folder') {
          console.log('Folder clicked:', item.data.name);
        } else {
          console.log('File clicked:', item.data.name);
        }
      }}
      renderLabel={(data) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          <span>{data.name}</span>
          {data.size && <span>({data.size}KB)</span>}
        </div>
      )}
    />
  );
}

2️⃣ Tree with Search

import { TreeWithSearch } from '@happyhyep/tree-component';

function SearchableTree() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <TreeWithSearch
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => setSelectedId(item.id)}
      renderLabel={(data, highlight) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          {/* 🔍 Automatic search term highlighting */}
          <HighlightText text={data.name} highlight={highlight} />
        </div>
      )}
      searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())}
    >
      {/* 🎯 Built-in search input */}
      <TreeWithSearch.Input placeholder="Search files..." />
    </TreeWithSearch>
  );
}

// Highlight helper component
const HighlightText = ({ text, highlight }) => {
  if (!highlight) return <span>{text}</span>;

  const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part,
      )}
    </span>
  );
};

3️⃣ Advanced Usage - Default Expand All

function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // 🚀 Expand all folders by default
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}

📋 Real-world Use Cases

File Explorer

import { Tree } from '@happyhyep/tree-component';

function FileExplorer() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleItemClick = (item) => {
    if (item.data.type === 'file') {
      // File click - open file
      openFile(item.data);
    } else {
      // Folder click - select folder (expand/collapse is automatic)
      setSelectedFolder(item.data);
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <Tree
        items={fileSystemData}
        onItemClick={handleItemClick}
        renderLabel={(data) => <FileIcon type={data.type} name={data.name} />}
      />
      {selectedFile && <FilePreview file={selectedFile} />}
    </div>
  );
}

Organization Chart / Hierarchy

function OrganizationChart() {
  return (
    <Tree
      items={orgData}
      defaultExpandAll={true}
      onItemClick={(item) => {
        // Both departments and employees are clickable
        showPersonDetails(item.data);
      }}
      renderLabel={(data) => (
        <div>
          <strong>{data.name}</strong>
          <span>({data.position})</span>
        </div>
      )}
    />
  );
}

🔧 API Documentation

TreeItem Interface

interface TreeItem<T = unknown> {
  id: string; // Unique identifier
  parentId: string | null; // Parent ID (null for root)
  data: T; // User data
  canOpen?: boolean; // Whether it can be expanded
  hasLeaf?: boolean; // Whether it's a leaf node
  children?: TreeItem<T>[]; // Child nodes (auto-generated)
}

Tree Props

| Props | Type | Required | Default | Description | | ------------------ | ----------------------------- | -------- | ------- | -------------------------------------- | | items | TreeItem<T>[] | ✅ | - | Tree data | | renderLabel | (data: T) => ReactNode | ✅ | - | Label rendering function | | onItemClick | (item: TreeItem<T>) => void | ❌ | - | Click handler (both folders/files) | | selectedId | string | ❌ | - | Selected item ID | | defaultExpandAll | boolean | ❌ | false | Expand all folders by default | | className | string | ❌ | "" | CSS class |

TreeWithSearch Props

All Tree props + additional:

| Props | Type | Required | Description | | ---------- | --------------------------------------- | -------- | ------------------- | | searchFn | (data: T, keyword: string) => boolean | ✅ | Search function | | children | ReactNode | ❌ | Search input, etc. |

💡 Tips and Tricks

1. Conditional Click Handling

const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // Folder click - special logic
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // File click - open file
    openFile(item);
  }
};

2. Custom Search

// Multi-condition search
const advancedSearch = (data, keyword) => {
  return (
    data.name.toLowerCase().includes(keyword.toLowerCase()) ||
    data.tags?.some((tag) => tag.includes(keyword)) ||
    data.content?.includes(keyword)
  );
};

// Extension search
const extensionSearch = (data, keyword) => {
  const extension = data.name.split('.').pop();
  return extension?.toLowerCase().includes(keyword.toLowerCase());
};

3. Performance Optimization

// Memoization for large datasets
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));

🎨 Styling

Using CSS Classes

<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}

🛠️ Development

Local Development Setup

# Clone repository
git clone https://github.com/happyhyep/tree-component.git
cd tree-component

# Install dependencies
pnpm install

# Run Storybook
pnpm run storybook

# Build
pnpm run build

# Lint
pnpm run lint

Storybook

Component documentation and examples are available in Storybook:

pnpm run storybook

🤝 Contributing

  1. Fork this repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

This project is licensed under the MIT License.

🙋‍♂️ Support


🇰🇷 한국어 ver

폴더와 파일 모두 클릭 가능한 React Tree 컴포넌트 라이브러리 입니다.

npm version License: MIT

✨ 주요 차별점

다른 Tree 컴포넌트와 달리 폴더와 파일 모두 클릭하여 상호작용할 수 있는 Tree 컴포넌트입니다.

| 기능 | 일반적인 Tree | 🌳 Tree Component | | -------------- | ----------------- | -------------------------------- | | 폴더 클릭 | ❌ 펼치기/접기만 | ✅ 클릭 이벤트 + 펼치기/접기 | | 파일 클릭 | ✅ 클릭 가능 | ✅ 클릭 가능 | | 검색 기능 | ❌ 별도 구현 필요 | ✅ 내장 검색 + 하이라이트 | | 기본 확장 상태 | ❌ 수동 설정 | ✅ 한 번에 모든 폴더 확장 | | TypeScript | ⚠️ 제한적 지원 | ✅ 완전한 타입 안전성 |

🎬 데모

Tree 컴포넌트 사용 예시

Basic Tree Demo

📦 설치

# npm
npm install @happyhyep/tree-component

# yarn
yarn add @happyhyep/tree-component

# pnpm
pnpm add @happyhyep/tree-component

🚀 빠른 시작

1️⃣ 기본 Tree 컴포넌트

import React, { useState } from 'react';
import { Tree, TreeItem } from '@happyhyep/tree-component';

interface FileData {
  name: string;
  type: 'folder' | 'file';
  size?: number;
}

const data: TreeItem<FileData>[] = [
  {
    id: '1',
    parentId: null,
    canOpen: true,
    data: { name: 'Documents', type: 'folder' },
  },
  {
    id: '2',
    parentId: '1',
    canOpen: false,
    data: { name: 'report.pdf', type: 'file', size: 1024 },
  },
  {
    id: '3',
    parentId: '1',
    canOpen: true,
    data: { name: 'Projects', type: 'folder' },
  },
];

function MyApp() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <Tree
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => {
        setSelectedId(item.id);
        console.log('클릭된 항목:', item.data);

        // 💡 폴더든 파일이든 모두 클릭 가능!
        if (item.data.type === 'folder') {
          console.log('폴더 클릭:', item.data.name);
        } else {
          console.log('파일 클릭:', item.data.name);
        }
      }}
      renderLabel={(data) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          <span>{data.name}</span>
          {data.size && <span>({data.size}KB)</span>}
        </div>
      )}
    />
  );
}

2️⃣ 검색 기능이 있는 Tree

import { TreeWithSearch } from '@happyhyep/tree-component';

function SearchableTree() {
  const [selectedId, setSelectedId] = useState<string>();

  return (
    <TreeWithSearch
      items={data}
      selectedId={selectedId}
      onItemClick={(item) => setSelectedId(item.id)}
      renderLabel={(data, highlight) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span>{data.type === 'folder' ? '📁' : '📄'}</span>
          {/* 🔍 검색어 자동 하이라이트 */}
          <HighlightText text={data.name} highlight={highlight} />
        </div>
      )}
      searchFn={(data, keyword) => data.name.toLowerCase().includes(keyword.toLowerCase())}
    >
      {/* 🎯 내장 검색 입력창 */}
      <TreeWithSearch.Input placeholder="파일명 검색..." />
    </TreeWithSearch>
  );
}

// 하이라이트 헬퍼 컴포넌트
const HighlightText = ({ text, highlight }) => {
  if (!highlight) return <span>{text}</span>;

  const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
  return (
    <span>
      {parts.map((part, i) =>
        part.toLowerCase() === highlight.toLowerCase() ? <mark key={i}>{part}</mark> : part,
      )}
    </span>
  );
};

3️⃣ 고급 사용법 - 모든 폴더 기본 확장

function ExpandedTree() {
  return (
    <Tree
      items={data}
      defaultExpandAll={true} // 🚀 모든 폴더 기본 확장
      renderLabel={(data) => <span>{data.name}</span>}
    />
  );
}

📋 실제 사용 사례

파일 탐색기

import { Tree } from '@happyhyep/tree-component';

function FileExplorer() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleItemClick = (item) => {
    if (item.data.type === 'file') {
      // 파일 클릭 시 - 파일 열기
      openFile(item.data);
    } else {
      // 폴더 클릭 시 - 폴더 선택 (펼치기/접기는 자동)
      setSelectedFolder(item.data);
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <Tree
        items={fileSystemData}
        onItemClick={handleItemClick}
        renderLabel={(data) => <FileIcon type={data.type} name={data.name} />}
      />
      {selectedFile && <FilePreview file={selectedFile} />}
    </div>
  );
}

조직도 / 계층 구조

function OrganizationChart() {
  return (
    <Tree
      items={orgData}
      defaultExpandAll={true}
      onItemClick={(item) => {
        // 부서든 직원이든 클릭 가능
        showPersonDetails(item.data);
      }}
      renderLabel={(data) => (
        <div>
          <strong>{data.name}</strong>
          <span>({data.position})</span>
        </div>
      )}
    />
  );
}

🔧 API 문서

TreeItem 인터페이스

interface TreeItem<T = unknown> {
  id: string; // 고유 식별자
  parentId: string | null; // 부모 ID (루트는 null)
  data: T; // 사용자 데이터
  canOpen?: boolean; // 펼칠 수 있는지 여부
  hasLeaf?: boolean; // 리프 노드 여부
  children?: TreeItem<T>[]; // 자식 노드 (자동 생성)
}

Tree Props

| Props | 타입 | 필수 | 기본값 | 설명 | | ------------------ | ----------------------------- | ---- | ------- | -------------------------------- | | items | TreeItem<T>[] | ✅ | - | 트리 데이터 | | renderLabel | (data: T) => ReactNode | ✅ | - | 라벨 렌더링 | | onItemClick | (item: TreeItem<T>) => void | ❌ | - | 클릭 핸들러 (폴더/파일 모두) | | selectedId | string | ❌ | - | 선택된 항목 ID | | defaultExpandAll | boolean | ❌ | false | 모든 폴더 기본 확장 | | className | string | ❌ | "" | CSS 클래스 |

TreeWithSearch Props

Tree의 모든 props + 추가:

| Props | 타입 | 필수 | 설명 | | ---------- | --------------------------------------- | ---- | -------------- | | searchFn | (data: T, keyword: string) => boolean | ✅ | 검색 함수 | | children | ReactNode | ❌ | 검색 입력창 등 |

💡 팁과 트릭

1. 조건부 클릭 처리

const handleClick = (item) => {
  if (item.data.type === 'folder') {
    // 폴더 클릭 - 특별한 로직
    if (item.data.permissions?.canAccess) {
      navigateToFolder(item);
    } else {
      showPermissionError();
    }
  } else {
    // 파일 클릭 - 파일 열기
    openFile(item);
  }
};

2. 커스텀 검색

// 다중 조건 검색
const advancedSearch = (data, keyword) => {
  return (
    data.name.toLowerCase().includes(keyword.toLowerCase()) ||
    data.tags?.some((tag) => tag.includes(keyword)) ||
    data.content?.includes(keyword)
  );
};

// 확장자 검색
const extensionSearch = (data, keyword) => {
  const extension = data.name.split('.').pop();
  return extension?.toLowerCase().includes(keyword.toLowerCase());
};

3. 성능 최적화

// 큰 데이터셋을 위한 메모이제이션
const MemoizedTree = React.memo(() => (
  <Tree
    items={largeDataSet}
    renderLabel={React.useCallback(
      (data) => (
        <span>{data.name}</span>
      ),
      [],
    )}
  />
));

🎨 스타일링

CSS 클래스 사용

<Tree
  className="my-custom-tree"
  items={data}
  renderLabel={(data) => <span className={`item-${data.type}`}>{data.name}</span>}
/>
.my-custom-tree {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.item-folder {
  font-weight: bold;
  color: #4a90e2;
}

.item-file {
  color: #666;
}

🛠️ 개발

로컬 개발 환경

# 저장소 클론
git clone https://github.com/happyhyep/tree-component.git
cd tree-component

# 의존성 설치
pnpm install

# Storybook 실행
pnpm run storybook

# 빌드
pnpm run build

# 린트
pnpm run lint

Storybook

컴포넌트 문서와 예제는 Storybook에서 확인할 수 있습니다:

pnpm run storybook

🤝 기여하기

  1. 이 저장소를 포크하세요
  2. 기능 브랜치를 만드세요 (git checkout -b feature/amazing-feature)
  3. 변경사항을 커밋하세요 (git commit -m 'Add amazing feature')
  4. 브랜치에 푸시하세요 (git push origin feature/amazing-feature)
  5. Pull Request를 열어주세요

📄 라이센스

이 프로젝트는 MIT 라이센스 하에 배포됩니다.

🙋‍♂️ 지원