Reactを用いた再帰的なコンポーネント実装例

エキサイト株式会社に所属しているエンジニアの久々江です。

本稿では、React（Next.js）を用いて再帰的なコンポーネントの実装例を示します。

:::note info
本稿は、[エキサイトホールディングス Advent Calendar 2023](https://qiita.com/advent-calendar/2023/excite-hd)の10日目の記事です。
:::

# 再帰とは？
ある処理の中でその処理自身が、呼び出される処理を再帰といいます。
したがって、再帰的なコンポーネントとは、あるコンポーネントの中でそのコンポーネント自体を呼び出すコンポーネントです。
ただし、無限に繰り返すわけでなく「表示したい子要素が無くなるまで生成」、「アコーディオン押下時に１行ずつ生成」など条件に応じてその繰り返しを制御します。

# 実装するUI
本稿では、再帰的なコンポーネントの中でも以下のような`TreeView`をReact（Next.js）を用いて実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679173/d26172d4-03c8-d00d-ec5c-24e99361d8fb.png)


# 使用技術

* 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`の構成です。
```bash
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
```

# データの用意
以下のような階層構造を持つデータを定数として宣言します。
```typescript:hierarchyItem
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`を構成するコンポーネントは２つに分けられます。1つは、再帰的な機能を担う`TreeView`コンポーネント、もう1つは個々のUIを担う任意のコンポーネント（ここでは仮に`TreeViewContent`とします）です。
`TreeView`コンポーネントは汎用コンポーネントとして運用しますが、`TreeViewContent`コンポーネントは呼び出し先に応じて定義することを想定しています。

## TreeViewコンポーネント実装
上述の通り、再帰的な機能を担います。具体的には子要素有無の確認とStateによる子要素の表示の制御を担います。

```tsx:TreeView.tsx
'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`を変更できます。

```tsx:TreeViewContent.tsx
'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
```

## 使用例
```tsx:page.tsx
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`コンポーネントを実装しました。
近しい実装を検討されている方の参考になれば幸いです。