0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

再利用性の高いコンポーネントを作成する

Posted at

再利用性の高いコンポーネントを作成する

汎用的なコンポーネントを作りたい!
でも、多すぎるpropsを渡していませんか?
以下の悪い例と良い例を見比べれば、
再利用性を高めるには、propsを適切に絞ることが重要だとわかるはず。

悪い例:

function Table({
  data,
  columns,
  sortable = false,
  filterable = false,
  pagination,
  rowActions = [],
  headerStyle,
  rowStyle,
  striped = false,
  onRowClick,
}) {
  ...
}

良い例

function Table<T>({ data, columns, onRowClick }) {...}

再利用性の高いテーブルコンポーネント作ってみよう!

以下は一般的なテーブルコンポーネントです。
propsとしてはdataとcolumnsだけを受け取っているのでバッチリOK。
(urlが渡されて、このコンポーネント内でフェッチしてもOK)

改善点はどこかな?

type Column =  {
  key: string;
  header: string;
}

type TableProps = {
  data: {
    id: number;
    name: string;
    price: string;
    stock: string;
  }[];
  columns: Column[];
}

const Table = ({ data, columns }: TableProps) => {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => (
            <th key="column.key">{column.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row) => (
          <tr key={row.id}>
            <td>{row.id}</td>
            <td>{row.name}</td>
            <td>{row.price}</td>
            <td>{row.stock}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default Table;

このままだとデータ構造が固定されてしまうね。
本当はどんなデータの形でも受け入れたいところ、、

リファクタリング1:ジェネリクスを使う

type Column<T> = {
  key: keyof T;
  header: string;
};

type TableProps<T> = {
  data: T[];
  columns: Column<T>[];
};

function Table<T extends { id: number }>({ data, columns }: TableProps<T>) {
  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={String(column.key)}>{String(row[column.key])}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default Table;

ジェネリクスを使用してデータ型を柔軟にしたよ。
セル(td)も繰り返し処理で記述できたよ。

import "./App.css";
import Table from "./components/Table";

type Product = {
  id: number;
  name: string;
  price: string;
  stock: string;
};

function App() {
  const columns: Array<{
    key: keyof Product;
    header: string;
  }> = [
    { key: "id", header: "ID" },
    { key: "name", header: "Name"},
    { key: "price", header: "Price" },
    { key: "stock", header: "Stock" },
  ];

  const products: Product[] = [
    { id: 1, name: "Wireless Mouse", price: "$25.99", stock: "In Stock" },
    {
      id: 2,
      name: "Mechanical Keyboard",
      price: "$75.49",
      stock: "Out of Stock",
    },
    { id: 3, name: "USB-C Hub", price: "$45.00", stock: "In Stock" },
    {
      id: 4,
      name: "Noise-Cancelling Headphones",
      price: "$199.99",
      stock: "In Stock",
    },
    { id: 5, name: "Webcam", price: "$89.99", stock: "Out of Stock" },
  ];

  return (
    <>
      <Table<Product> columns={columns} data={products} />
    </>
  );
}

export default App;

親コンポーネントでは型を定義し、型を子コンポーネントに渡しているよ。

リファクタリング2: セルに入る内容を柔軟化

import { ReactNode } from "react";
import "./App.css";
import Table from "./components/Table";

type Product = {
  id: number;
  name: string;
  price: string;
  stock: string;
};

function App() {
  const columns: Array<{
    key: keyof Product;
    header: string;
    render?: (value: Product[keyof Product]) => ReactNode;
  }> = [
    { key: "id", header: "ID" },
    { key: "name", header: "Name", render: (value) => <a href="#">{value}</a> },
    { key: "price", header: "Price" },
    { key: "stock", header: "Stock" },
  ];

  const products: Product[] = [
    { id: 1, name: "Wireless Mouse", price: "$25.99", stock: "In Stock" },
    {
      id: 2,
      name: "Mechanical Keyboard",
      price: "$75.49",
      stock: "Out of Stock",
    },
    { id: 3, name: "USB-C Hub", price: "$45.00", stock: "In Stock" },
    {
      id: 4,
      name: "Noise-Cancelling Headphones",
      price: "$199.99",
      stock: "In Stock",
    },
    { id: 5, name: "Webcam", price: "$89.99", stock: "Out of Stock" },
  ];

  return (
    <>
      <Table<Product> columns={columns} data={products} />
    </>
  );
}

export default App;

columnsにオプショナルなrenderプロパティを追加したよ。
これで文字列に限らずなんでも入るようになったね。
この例では、リンクが入るようにしたよ。

import { ReactNode } from "react";

type Column<T> = {
  key: keyof T;
  header: string;
  render?: (value: T[keyof T]) => ReactNode;
};

type TableProps<T> = {
  data: T[];
  columns: Column<T>[];
};

function Table<T extends { id: number }>({ data, columns }: TableProps<T>) {
  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={String(column.key)}>
                {column.render
                  ? column.render(row[column.key])
                  : String(row[column.key])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default Table;

renderプロパティの有無でセルの中身の表示を変えているよ。

リファクタリング3:コンポーネントを細分化する

import { ReactNode } from "react";

type Column<T> = {
  key: keyof T;
  header: string;
  render?: (value: T[keyof T]) => ReactNode;
};

type TableProps<T> = {
  data: T[];
  columns: Column<T>[];
};

export default function Table<T extends { id: number | string }>({
  data,
  columns,
}: TableProps<T>) {
  return (
    <div>
      <table>
        <TableHeader columns={columns} />
        <tbody>
          {data.map((row) => (
            <TableRow key={row.id} row={row} columns={columns} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

type TableHeaderProps<T> = {
  columns: Column<T>[];
};

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

type TableRowProps<T> = {
  row: T;
  columns: Column<T>[];
};

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

type TableCellProps<T> = {
  value: T;
  render?: (value: T) => ReactNode;
};

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

Tableコンポーネントを以下のように分けたよ。

  • TableHeader
  • TableRow
  • TableCell

これで読みやすいし管理されているね。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?