再利用性の高いコンポーネントを作成する
汎用的なコンポーネントを作りたい!
でも、多すぎる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
これで読みやすいし管理されているね。