2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React + Typescriptで型安全にColumnとRowを指定できるテーブルComponentを作成してみた

Last updated at Posted at 2021-09-14

前書き

  • 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を使って設計を考えてみました。

ご意見ある方いましたら、よければご教授いただけたら嬉しいです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?