@happyhyep/tree-component
v1.0.6
Published
React Tree Component with Search functionality
Maintainers
Readme
🌳 @happyhyep/tree-component
🇺🇸 English ver
React Tree Component Library where both folders and files are clickable
✨ 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

📦 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 lintStorybook
Component documentation and examples are available in Storybook:
pnpm run storybook🤝 Contributing
- Fork this repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License.
🙋♂️ Support
- 🐛 Bug Reports: GitHub Issues
- 💡 Feature Requests: GitHub Discussions
- 📧 Email: [email protected]
🇰🇷 한국어 ver
폴더와 파일 모두 클릭 가능한 React Tree 컴포넌트 라이브러리 입니다.
✨ 주요 차별점
다른 Tree 컴포넌트와 달리 폴더와 파일 모두 클릭하여 상호작용할 수 있는 Tree 컴포넌트입니다.
| 기능 | 일반적인 Tree | 🌳 Tree Component | | -------------- | ----------------- | -------------------------------- | | 폴더 클릭 | ❌ 펼치기/접기만 | ✅ 클릭 이벤트 + 펼치기/접기 | | 파일 클릭 | ✅ 클릭 가능 | ✅ 클릭 가능 | | 검색 기능 | ❌ 별도 구현 필요 | ✅ 내장 검색 + 하이라이트 | | 기본 확장 상태 | ❌ 수동 설정 | ✅ 한 번에 모든 폴더 확장 | | TypeScript | ⚠️ 제한적 지원 | ✅ 완전한 타입 안전성 |
🎬 데모
Tree 컴포넌트 사용 예시

📦 설치
# 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 lintStorybook
컴포넌트 문서와 예제는 Storybook에서 확인할 수 있습니다:
pnpm run storybook🤝 기여하기
- 이 저장소를 포크하세요
- 기능 브랜치를 만드세요 (
git checkout -b feature/amazing-feature) - 변경사항을 커밋하세요 (
git commit -m 'Add amazing feature') - 브랜치에 푸시하세요 (
git push origin feature/amazing-feature) - Pull Request를 열어주세요
📄 라이센스
이 프로젝트는 MIT 라이센스 하에 배포됩니다.
🙋♂️ 지원
- 🐛 버그 리포트: GitHub Issues
- 💡 기능 요청: GitHub Discussions
- 📧 이메일: [email protected]
