🍱

再帰処理で入れ子のテーブルコンポーネントを作る(折りたたみ機能付き!)

2021/08/08に公開

はじめに

再帰的なデータ構造に従って入れ子の(ネストされた)コンポーネントを実装してみました。

ミニマルな再帰コンポーネント

まずは、再帰処理の流れを掴むためにミニマルなコンポーネントの例を以下に示します。
実態としては、繰り返し {data.name} を表示していくだけですね。

type Data = { name: string; children?: Data[] };

type Props = {
  data: Data;
};

const RecursiveComponent = ({ data }: Props) => {
  return (
    <>
      <div>{data.name}</div>
      {data.children?.map((v) => {
        return <RecursiveComponent key={v.name} data={v} />;
      })}
    </>
  );
};

codesandobox も用意しました。手元で試せます!(注: サードパーティ cookie を無効にしていると見られません)

再帰的な入れ子テーブル

本記事のメインであるテーブルコンポーネントです。
折りたたみ機能も local state だけで済んでシンプルなところが気に入っています。

type User = {
  name: string;
  id: string;
  favoriteColor: string;
  children?: User[];
};

type UserRowProps = { user: User; level: number };

// 再帰処理しているコンポーネント
const UserRow = ({ user, level }: UserRowProps) => {
  const [expanded, setExpanded] = useState(false);
  // 階層を表現するために色と余白をつける
  const rgb = 255 - 10 * level;
  const color = `rgb(${rgb}, ${rgb}, ${rgb})`;
  const space = `${level ? 8 + 4 * level : 8}px`;

  return (
    <>
      <tr style={{ backgroundColor: color }}>
        <td style={{ paddingLeft: space }}>{user.name}</td>
        <td>{user.id}</td>
        <td>{user.favoriteColor}</td>
        <td>
          {user.children?.length ? (
            <button onClick={() => setExpanded((current) => !current)}>
              {expanded ? "↑" : "↓"}
            </button>
          ) : null}
        </td>
      </tr>
      {expanded &&
        user.children?.map((child) => {
          return (
            <UserRow
              key={`${child.name}-${child.id}`}
              user={child}
              level={level + 1}
            />
          );
        })}
    </>
  );
};

type UserTableProps = {
  users: User[];
};

const UserTable = ({ users }: UserTableProps) => {
  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>ID</th>
          <th>Favorite Color</th>
          <th />
        </tr>
      </thead>
      <tbody>
        {users.map((user) => (
          <UserRow key={`${user.name}-${user.id}`} user={user} level={0} />
        ))}
      </tbody>
    </table>
  );
};

<UserRow> 内で再帰処理を実行しています。
「入れ子」とは言っていますが、DOM 構造としては <tr> がフラットに並んでいく感じになります。

<tbody>
  <tr>...</tr>
  <tr>...</tr>
  <tr>...</tr>
  <tr>...</tr>
  
</tbody>

こちらも codesandbox を用意しました。

おわりに

「再帰」と聞くと何だか複雑そうなイメージがありましたが。、多少克服できた気がします。やはり案ずるより産むが易しですね。
ぜひ「いいね!」をいただけると嬉しいです。
以上、お読みいただき、ありがとうございました。

Discussion