😑

Reactで再利用可能なコンポーネントを作成するためのガイド

2025/03/09に公開

はじめに

Reactでアプリケーションを作る際に最も重要なスキルの一つは、再利用可能なコンポーネントを効率的に作成する能力です。これはプロジェクトの規模や複雑さに関わらず、高品質なアプリケーションを構築する上で不可欠です。

そのため、本記事では再利用可能なコンポーネントを作成するための具体的なテクニックとアプローチについて解説します。

なぜ再利用可能なコンポーネントが重要なのか

再利用可能なコンポーネントは、開発の効率性と一貫性を大幅に向上させます。メリットは以下の通りです。

再利用可能なコンポーネント設計の基本原則

続いて再利用可能なコンポーネント設計の基本原則について解説していきます。

1. 単一責任の原則を守る

各コンポーネントは一つのことに集中すべきです。コンポーネントが多くの責任を持つほど、再利用しにくくなります。

2. 柔軟なpropsインターフェースを設計する

ハードコードされた値を避け、props を通じて設定できるようにします。これにより、さまざまなコンテキストで同じコンポーネントを使用できるようになります。

3. 柔軟なレンダリング方法を提供する

データの表示方法をカスタマイズできるようにします。これにより、親コンポーネントがより多くの制御を持てるようになります。

4. シンプルなインターフェースを維持する

必要最小限の props で設計し、複雑さを隠蔽します。シンプルな API を持つコンポーネントほど使いやすくなります。

実践例:データテーブルコンポーネント

これから、再利用可能なデータテーブルコンポーネントを例に、設計と実装の過程を見ていきましょう。

基本的なデータテーブルの実装

最初のステップとして、シンプルなデータテーブルコンポーネントを作成します:

// /src/components/DataTable.tsx
type DataTableProps = {
  data: any[];
};

export const DataTable = ({ data }: DataTableProps) => {
  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>名前</th>
          <th>メール</th>
          <th>ロール</th>
        </tr>
      </thead>
      <tbody>
        {data.map((row) => (
          <tr key={row.id}>
            <td>{row.id}</td>
            <td>{row.name}</td>
            <td>{row.email}</td>
            <td>{row.role}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

この実装には明らかな問題があります

  • ヘッダーがハードコードされている
  • 表示する列が固定されている
  • データの表示方法がカスタマイズできない

このコンポーネントは特定のデータ構造にのみ対応しており、再利用性が非常に限られています。

コンポーネントの責任範囲を決める

再利用可能なコンポーネントを設計する際の最初のステップは、そのコンポーネントの責任範囲を明確にすることです。

データをどこから取得するか?

データテーブルの場合、データを取得する方法について 2 つの選択肢があります。

  1. 親コンポーネントから props としてデータを受け取る

    • より柔軟性が高い
    • 親コンポーネントがデータの管理を担当
    • さまざまなデータソースと使用できる
  2. コンポーネント内でデータを取得する(例:URL を渡して自動的にフェッチ)

    • 使用が簡単
    • 特定の用途に特化
    • 再利用性が限定される

この決定はコンポーネントの設計全体に影響します。今回は、より再利用性の高い 1 番目のアプローチを採用します。

コンポーネントをより柔軟にするためのテクニック

設定可能な列定義

まず、表示する列を親コンポーネントが定義できるようにします:

// 改良版 DataTable.tsx
type Column = {
  key: string;
  header: string;
};

type DataTableProps = {
  data: any[];
  columns: Column[];
};

export const DataTable = ({ data, columns }: DataTableProps) => {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => (
            <th key={column.key}>{column.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row) => (
          <tr key={row.id}>
            {columns.map((column) => (
              <td key={column.key}>{row[column.key]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

使用例

const App = () => {
  const data = [
    /* ... */
  ];
  const columns = [
    { key: "id", header: "ID" },
    { key: "name", header: "名前" },
    { key: "email", header: "メール" },
    { key: "role", header: "ロール" },
  ];

  return <DataTable data={data} columns={columns} />;
};

これにより、親コンポーネントが表示する列とそのヘッダーを自由に設定できるようになりました。

カスタムレンダリング関数の導入

次に、各セルのレンダリング方法をカスタマイズできるようにします。これはレンダープロップパターンを使用して実現します

// 型定義を追加
interface Column<T> {
  key: keyof T;
  header: string;
  render?: (value: T[keyof T], item: T) => React.ReactNode;
}

interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
}

// ジェネリック型を使用して、さまざまなデータ型に対応
export const DataTable = <T,>({ data, columns }: DataTableProps<T>) => {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => (
            <th key={String(column.key)}>{column.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, rowIndex) => (
          <tr key={rowIndex}>
            {columns.map((column) => (
              <td key={String(column.key)}>
                {column.render
                  ? column.render(row[column.key], row)
                  : String(row[column.key])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

使用例:

const App = () => {
  const data = [
    /* ... */
  ];
  const columns = [
    { key: "id", header: "ID" },
    { key: "name", header: "名前" },
    {
      key: "email",
      header: "メール",
      render: (value) => <a href={`mailto:${value}`}>{value}</a>,
    },
    { key: "role", header: "ロール" },
  ];

  return <DataTable data={data} columns={columns} />;
};

これで、メールアドレスをリンクとして表示するなど、特定の列の表示方法をカスタマイズできるようになりました。

サブコンポーネントへの分割

コンポーネントが大きくなりすぎると、メンテナンスが難しくなります。そこで、機能ごとに小さなサブコンポーネントに分割します:

// 型定義
interface Column<T> {
  key: keyof T;
  header: string;
  render?: (value: T[keyof T], item: T) => React.ReactNode;
}

// TableHeader コンポーネント
interface TableHeaderProps<T> {
  columns: Column<T>[];
}

const TableHeader = <T,>({ columns }: TableHeaderProps<T>) => {
  return (
    <thead>
      <tr>
        {columns.map((column) => (
          <th key={String(column.key)}>{column.header}</th>
        ))}
      </tr>
    </thead>
  );
};

// TableRow コンポーネント
interface TableRowProps<T> {
  row: T;
  columns: Column<T>[];
}

const TableRow = <T,>({ row, columns }: TableRowProps<T>) => {
  return (
    <tr>
      {columns.map((column) => (
        <TableCell
          key={String(column.key)}
          value={row[column.key]}
          render={column.render}
          row={row}
        />
      ))}
    </tr>
  );
};

// TableCell コンポーネント
interface TableCellProps<T> {
  value: T[keyof T];
  render?: (value: T[keyof T], item: T) => React.ReactNode;
  row: T;
}

const TableCell = <T,>({ value, render, row }: TableCellProps<T>) => {
  return <td>{render ? render(value, row) : String(value)}</td>;
};

// メインの DataTable コンポーネント
interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
}

const DataTable = <T,>({ data, columns }: DataTableProps<T>) => {
  return (
    <table>
      <TableHeader columns={columns} />
      <tbody>
        {data.map((row, index) => (
          <TableRow key={index} row={row} columns={columns} />
        ))}
      </tbody>
    </table>
  );
};

export default DataTable;

このように分割することで:

  1. 各コンポーネントが単一の責任を持つようになる
  2. コードが読みやすくなる
  3. 必要に応じて特定の部分だけを変更しやすくなる
  4. テストがしやすくなる

メインの DataTable コンポーネントはシンプルなままで、複雑さは各サブコンポーネントに分散されています。

まとめ

再利用可能なコンポーネントを作成するためのキーポイント

  1. 明確な責任範囲を定義する

    • コンポーネントが何をすべきか、何をすべきでないかを明確にする
  2. ハードコードされた値を避ける

    • すべての重要な値は props として渡せるようにする
  3. カスタマイズのためのpropsを設計する

    • レンダリング関数やカスタムコンポーネントを受け入れる仕組みを提供する
  4. 適切な抽象化レベルを選択する

    • 過度に抽象化されたコンポーネントは理解しにくく、十分に抽象化されていないコンポーネントは再利用性が低い
  5. シンプルなインターフェースを維持する

    • 必要最小限の props でコンポーネントを設計する
    • 複雑な処理はコンポーネント内部で行い、使用者からは隠蔽する
  6. サブコンポーネントに分割する

    • 単一責任の原則に従い、機能ごとに分割する

再利用可能なコンポーネントを作成するスキルは、Reactの開発において非常に重要です。これらのテクニックを活用することで、効率的でメンテナンスしやすく、一貫性のあるアプリケーションを構築することができます。

Discussion