LoginSignup
1
2

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

Last updated at Posted at 2023-12-11

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

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

本稿は、エキサイトホールディングス Advent Calendar 2023の10日目の記事です。

再帰とは?

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

実装するUI

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

image.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の構成です。

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

データの用意

以下のような階層構造を持つデータを定数として宣言します。

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を構成するコンポーネントは2つに分けられます。1つは、再帰的な機能を担うTreeViewコンポーネント、もう1つは個々のUIを担う任意のコンポーネント(ここでは仮にTreeViewContentとします)です。
TreeViewコンポーネントは汎用コンポーネントとして運用しますが、TreeViewContentコンポーネントは呼び出し先に応じて定義することを想定しています。

TreeViewコンポーネント実装

上述の通り、再帰的な機能を担います。具体的には子要素有無の確認とStateによる子要素の表示の制御を担います。

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を変更できます。

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

使用例

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2