Gemcook Tech Blog
🔁

Reactで再帰的なコンポーネントを作ってみた

2024/07/09に公開
2

はじめに

こんにちは!😄
社内でTypeChallengeを元に勉強会をしているのですが、再帰的に型づけする問題に取り組んでいると、ふとこんなことを思いました。(ちなみに問題はこれです。)
https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited

「再帰的に型づけしたデータを使って何か作ってみたいなあ。そういえば、再帰的なコンポーネントも実装したことないな…作るか!🔥」

一見複雑そうに思える再帰的なコンポーネントですが、実装してみると意外と単純な構造であることが分かり、そこでの学びを記事にしましたのでよければ見ていってください!

実装編

今回はシンプルにツリー構造のデータをリストとして出すような簡単なコンポーネントを作成します。完成系は以下になります。

データに型をつける

まず、ツリー構造のデータに対して型づけを行います。以下のようにTree型を定義します。

ポイントとなるのはTreeのプロパティであるbranchの型はTree[]であることです。子要素を示すbranchに対してTree[]を指定することで再帰的に型づけをします。子要素がない場合も存在するためオプショナルにしておきます。(オプショナルにしない場合、無限ループに陥るのでご注意を...!!)

data.ts
export type Tree = {
  id: string;
  name: string;
  branch?: Tree[];
};

export const TREE_LIST: Tree[] = [
  {
    id: "A",
    name: "Category A",
    branch: [
      {
        id: "A1",
        name: "Subcategory A1",
        branch: [
          { id: "A1-1", name: "Item A1-1" },
          { id: "A1-2", name: "Item A1-2" },
          { id: "A1-3", name: "Item A1-3" },
        ],
      },
      {
        id: "A2",
        name: "Subcategory A2",
        branch: [
          { id: "A2-1", name: "Item A2-1" },
          { id: "A2-2", name: "Item A2-2" },
          { id: "A2-3", name: "Item A2-3" },
        ],
      },
    ],
  },
  {
    id: "B",
    name: "Category B",
    branch: [
      {
        id: "B1",
        name: "Subcategory B1",
        branch: [
          { id: "B1-1", name: "Item B1-1" },
          { id: "B1-2", name: "Item B1-2" },
        ],
      },
    ],
  },
  {
    id: "C",
    name: "Category C",
    branch: [
      { id: "C1", name: "Item C1" },
      { id: "C2", name: "Item C2" },
      // 省略
    ],
  },
];

コンポーネントを作成する

次に、実際にツリー構造のデータを表示するコンポーネントを作成します。

ここでのポイントはTreeListコンポーネントの中で、再びTreeListを呼び出していることです。子要素であるbranchがある場合かつ、リストを開いている場合は、次の階層データを表示させます。(階層はlevelで指定しています。)これで再帰的にコンポーネントを呼び出すことができます。

TreeList.tsx
import React, { useState } from "react";
import { Tree } from "./data";

type Props = {
  items: Tree[];
  level: number;
};

export const TreeList: React.FC<Props> = ({ items, level }) => {
  const [openItems, setOpenItems] = useState<{
    [key: Tree["id"]]: boolean;
  }>({});

  const toggleItem = (id: Tree["id"]) => {
    setOpenItems((prevState) => ({
      ...prevState,
      [id]: !prevState[id],
    }));
  };

  return (
    <>
      {items.map((item) => (
        <div key={item.id}>
          <div onClick={() => item.branch && toggleItem(item.id)}>
            {item.branch ? (openItems[item.id] ? "▼ " : "▶ ") : "●"}
            {item.name}
          </div>
          {item.branch && openItems[item.id] && (
            <TreeList items={item.branch} level={level + 1} />
          )}
        </div>
      ))}
    </>
  );
};

※スタイルの指定は省略しています。

作成したTreeListコンポーネントを以下のように呼び出すだけで完成です。非常にシンプルですね。

index.tsx
import { TREE_LIST } from "./data";
import { TreeList } from "./TreeList";

export default function Index() {
  return <TreeList items={TREE_LIST} level={0} />;
}

最後に

再帰的なコンポーネントはデータのツリー構造を表現したり、ネストされたコンテンツを効率的に扱ったりするのに非常に有用だなと感じました。

また、一見再帰的なコンポーネントは複雑なように思えますが、仕組みとしては単純な構造であることが分かりました。今回は実装しませんでしたが階層ごとに機能を追加するなど、構造が単純であるがゆえにカスタマイズするのは大変なのかもなあとも思いました。(またどこかでチャレンジしてみようと思います!)

最後までお読みいただきありがとうございました!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion

Naoki IwataNaoki Iwata

index.tsxにimportが書かれていないのと
TREE_LISTTREE_LIS2 になっているので
修正したほうが良さそう。

こういう記述を省略するとミスをしやすくなる。

import { TreeList } from './TreeList';
import { TREE_LIST } from './data';

export default function Index() {
  return <TreeList items={TREE_LIST} level={0} />;
}
KMKM

こういう記述を省略するとミスをしやすくなる。

まったく持ってその通りですね...!!
ありがとうございます!🙇