エキサイト株式会社に所属しているエンジニアの久々江です。
本稿では、React(Next.js)を用いて再帰的なコンポーネントの実装例を示します。
本稿は、エキサイトホールディングス Advent Calendar 2023の10日目の記事です。
再帰とは?
ある処理の中でその処理自身が、呼び出される処理を再帰といいます。
したがって、再帰的なコンポーネントとは、あるコンポーネントの中でそのコンポーネント自体を呼び出すコンポーネントです。
ただし、無限に繰り返すわけでなく「表示したい子要素が無くなるまで生成」、「アコーディオン押下時に1行ずつ生成」など条件に応じてその繰り返しを制御します。
実装するUI
本稿では、再帰的なコンポーネントの中でも以下のようなTreeView
をReact(Next.js)を用いて実装します。
使用技術
- Node.js: 20.10.0
- Next.js: 14.0.1
- React: 18.2.0
- TypeScript: 5.2.2
ディレクトリ構成
TreeView
コンポーネントの作成にあたり、Next.js(AppRouter)を用いておりsrc
ディレクトリを設けています。
本稿では、その中でもTreeView
に関係のあるファイルを解説します。
以下はそのsrc
の構成です。
src
├── app
│ ├── _components //ページの専用コンポーネント
│ │ ├── TreeViewContent
│ │ │ ├── TreeViewContent.module.css
│ │ │ ├── TreeViewContent.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
└── components //汎用コンポーネント
└── common
├── Card //本稿での解説は省略
│ ├── Card.module.css
│ ├── Card.tsx
│ └── index.ts
├── TreeView
│ ├── TreeView.module.css
│ ├── TreeView.tsx
│ └── index.ts
└── index.ts
データの用意
以下のような階層構造を持つデータを定数として宣言します。
const hierarchyItem = {
id: 1,
name: 'node 1',
children: [
{
id: 11,
name: 'node 11',
children: [
{
id: 111,
name: 'node 111',
},
],
},
{
id: 12,
name: 'node 12',
children: [
{
id: 121,
name: 'node 121',
},
{
id: 122,
name: 'node 122',
},
],
},
],
}
TreeViewコンポーネント実装
再利用性を考慮すると、TreeView
を構成するコンポーネントは2つに分けられます。1つは、再帰的な機能を担うTreeView
コンポーネント、もう1つは個々のUIを担う任意のコンポーネント(ここでは仮にTreeViewContent
とします)です。
TreeView
コンポーネントは汎用コンポーネントとして運用しますが、TreeViewContent
コンポーネントは呼び出し先に応じて定義することを想定しています。
TreeViewコンポーネント実装
上述の通り、再帰的な機能を担います。具体的には子要素有無の確認とStateによる子要素の表示の制御を担います。
'use client'
import { Dispatch, FC, SetStateAction, useState } from 'react'
import s from './TreeView.module.css'
type RecursiveItem = {
id: number
children?: RecursiveItem[]
[key: string]: unknown
}
type TreeViewProps = {
markup: FC<{
item: RecursiveItem
isOpened: boolean
setIsOpened: Dispatch<SetStateAction<boolean>>
}>
item: RecursiveItem
}
const TreeView: FC<TreeViewProps> = (props) => {
const { markup, item } = props
const [isOpened, setIsOpened] = useState(true)
return (
<div>
{markup({ item, isOpened, setIsOpened })}
{isOpened && (
<div className={s.children}>
{item.children?.map((child) => <TreeView markup={markup} item={child} key={child.id} />)}
</div>
)}
</div>
)
}
export default TreeView
TreeViewContentコンポーネント実装
このコンポーネントは任意のUIを定義するためのものです。ただし、TreeView
にて定義されているmarkup
のTypeに従い、表示のState更新をトリガーする必要はあります。
TreeViewContent
はその一例であり、TreeView
の呼び出し先に応じてmarkup
を変更できます。
'use client'
import { Dispatch, FC, SetStateAction } from 'react'
import s from './TreeViewContent.module.css'
type RecursiveItem = {
id: number
children?: RecursiveItem[]
[key: string]: unknown
}
const TreeViewContent: FC<{
item: RecursiveItem
isOpened: boolean
setIsOpened: Dispatch<SetStateAction<boolean>>
}> = (props) => {
const { item, isOpened, setIsOpened } = props
return (
<div className={s.root}>
{item.children && item.children.length > 0 && (
<button onClick={() => setIsOpened(!isOpened)} className={s.accordion}>
<span className="material-symbols-outlined">expand_more</span>
</button>
)}
<span>{`${item.name}`}</span>
</div>
)
}
export default TreeViewContent
使用例
import { Card, TreeView } from '@/components/common'
import { TreeViewContent } from './_components'
export default function Home() {
const hierarchyItem = {
id: 1,
name: 'node 1',
children: [
{
id: 11,
name: 'node 11',
children: [
{
id: 111,
name: 'node 111',
},
],
},
{
id: 12,
name: 'node 12',
children: [
{
id: 121,
name: 'node 121',
},
{
id: 122,
name: 'node 122',
},
],
},
],
}
return (
<div className="page-container">
<Card>
<TreeView markup={TreeViewContent} item={hierarchyItem} />
</Card>
</div>
)
}
まとめ
本稿では、Reactを用いて再帰的なコンポーネントの一例としてTreeView
コンポーネントを実装しました。
近しい実装を検討されている方の参考になれば幸いです。