前書き
- Material-uiを使ったテーブルを作成したい
- 共通仕様にできるコンポーネントとして独自設計したい
- ColumnとRowをObject型、かつPropsとして指定する設計にする
- ColumnとRowの対応関係をなるべく型安全に定義したい
これらを満たすテーブルコンポーネントをReact + TypeScript
で 作成してみました。
技術
- React 17.0.2
- TypeScript 4.4.2
- Material-ui/core 4.12.3
コードでは以下のExampleをラップしたコンポーネントを作成
https://material-ui.com/components/tables/#fixed-header
コード
Component
type TColumns<T extends string> = {
id: T;
label: string;
width?: number;
};
type TRows<T extends string> = {
[key in T]: string | number | React.FC;
};
type Props<T extends string> = {
columns: TColumns<T>[];
rows: TRows<T>[];
};
/** ※公式引用 */
const useStyles = makeStyles({
root: {
width: "100%",
},
container: {
maxHeight: 440,
},
});
const GenericTable = <T extends string>(props: Props<T>): JSX.Element => {
const classes = useStyles();
const [page, setPage] = React.useState<number>(0);
const [rowsPerPage, setRowsPerPage] = React.useState<number>(10);
return (
<>
<TableContainer className={classes.container}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
{props.columns.map((column) => (
<TableCell key={column.id} align="left" style={{ minWidth: column.width }}>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, i) => {
return (
<TableRow hover role="checkbox" tabIndex={-1} key={i}>
{props.columns.map((column) => {
return (
<TableCell key={column.id} align={column.align}>
{row[column.id]}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={10}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={(event: any, newPage: number) => {
setPage(newPage);
}}
onRowsPerPageChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(+event.target.value);
setPage(0);
}}
labelDisplayedRows={({ from, to, count }) => {
return `${from}から${to} 件 / ${count}件中`;
}}
labelRowsPerPage={<>表示件数</>}
/>
</>
);
};
export default GenericTable;
使用例
import GenericTable, { TColumns, TRows } from "...GenericTable";
// カラム値を定義
type ColumnType = "id" | "name";
const columns: TColumns<ColumnType>[] = [
{ id: "id", label: "ユーザーID", width: 240 },
{ id: "name", label: "ユーザー名", width: 240 },
];
const rows: TRows<ColumnType>[] = [
{ id: 1, name: "トム" },
{ id: 2, name: "ブラウン" },
];
// コンポーネント呼び出し
<GenericTable<ColumnType> columns={columns} rows={rows} />
良い点
- 何を列としているのか分かり易い
- 依存性を1つ(=
ColumnType
)に集約できる - 以下のような事象をエラー検知できる
type ColumnType = "id" | "name";
// カラムに定義外のidが入る
const columns: TColumns<ColumnType>[] = [
{ id: "id", label: "ユーザーID", width: 240 },
{ id: "name", label: "ユーザー名", width: 240 },
{ id: "neta", label: "ネタ", width: 240 }, // ダメ〜
];
// データに定義外のカラム値を混ぜる
const rows: TRows<ColumnType>[] = [
{ id: 1, name: "トム", neta: "エラーを" },
{ id: 2, name: "ブラウン", neta: "放置します" }, // ダメ〜
];
微妙な点
- ColumnTypeなる型を別途定義する必要がある
- 重複するid値を持つなどのケースはエラー検知できない
type ColumnType = "id" | "name";
// 例
const columns: TColumns<ColumnType>[] = [
{ id: "id", label: "ユーザーID", width: 240 },
{ id: "name", label: "ユーザー名", width: 240 },
{ id: "name", label: "ユーザー名2", width: 240 }, // エラー出ない
];
後書き
Tableコンポーネントを作る機会があったので、
TypeScriptのGenericsを使って設計を考えてみました。
ご意見ある方いましたら、よければご教授いただけたら嬉しいです。