Reactで再帰的なコンポーネントを作ってみた
はじめに
こんにちは!😄
社内でTypeChallengeを元に勉強会をしているのですが、再帰的に型づけする問題に取り組んでいると、ふとこんなことを思いました。(ちなみに問題はこれです。)
「再帰的に型づけしたデータを使って何か作ってみたいなあ。そういえば、再帰的なコンポーネントも実装したことないな…作るか!🔥」
一見複雑そうに思える再帰的なコンポーネントですが、実装してみると意外と単純な構造であることが分かり、そこでの学びを記事にしましたのでよければ見ていってください!
実装編
今回はシンプルにツリー構造のデータをリストとして出すような簡単なコンポーネントを作成します。完成系は以下になります。
データに型をつける
まず、ツリー構造のデータに対して型づけを行います。以下のようにTree型を定義します。
ポイントとなるのはTree
のプロパティであるbranch
の型はTree[]
であることです。子要素を示すbranch
に対してTree[]
を指定することで再帰的に型づけをします。子要素がない場合も存在するためオプショナルにしておきます。(オプショナルにしない場合、無限ループに陥るのでご注意を...!!)
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
で指定しています。)これで再帰的にコンポーネントを呼び出すことができます。
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
コンポーネントを以下のように呼び出すだけで完成です。非常にシンプルですね。
import { TREE_LIST } from "./data";
import { TreeList } from "./TreeList";
export default function Index() {
return <TreeList items={TREE_LIST} level={0} />;
}
最後に
再帰的なコンポーネントはデータのツリー構造を表現したり、ネストされたコンテンツを効率的に扱ったりするのに非常に有用だなと感じました。
また、一見再帰的なコンポーネントは複雑なように思えますが、仕組みとしては単純な構造であることが分かりました。今回は実装しませんでしたが階層ごとに機能を追加するなど、構造が単純であるがゆえにカスタマイズするのは大変なのかもなあとも思いました。(またどこかでチャレンジしてみようと思います!)
最後までお読みいただきありがとうございました!
Discussion
index.tsxにimportが書かれていないのと
TREE_LIST
がTREE_LIS2
になっているので修正したほうが良さそう。
こういう記述を省略するとミスをしやすくなる。
まったく持ってその通りですね...!!
ありがとうございます!🙇