APIとReact Table の連携を TypeScript で型付けしながら書いていく

こんにちはフロントエンドエンジニアの茶木です。
最近は学習用の Next.js の ローカルの SandBox を育てています。

前回、APIのコールとデータの受け取りについて書きました。今回はこのAPIで取得したデータを元にテーブル(表)を作ります。

下準備

React Table

https://react-table-v7.tanstack.com/

React Table v7 を使います。 v8 もリリースされているのですが、v7 とは使い勝手が違います。今回はv7 をインストールします。

yarn add react-table

MUI

https://mui.com/

最小限の装飾のために MUI の MuiTable を使用します。
React Table との相性も良いです。こちらもインストールしておきます。

yarn add @mui/material @emotion/react @emotion/styled

モックAPI

Next.js サポートの API route を利用してモックAPIを作ります。API route については前回記事で詳しく触れています。 例としてUser 情報を取得する APIを作成します。

BaseRow

テーブルの1行のフォーマットです。最低限 id を持ち、さらにキーと値のペア Record<string, unknown> をいくつでもなんでも許容します。

export type BaseRow = {
  id: string;
} & Record<string, unknown>;

TableData

BaseRow の形式に則ったRowをジェネリクスで渡しています。Rowの配列がテーブルの本体になります。今記事ではスキップしますが、TableData には全件数など付加的な情報を渡す想定です。

export interface TableData<Row extends BaseRow> {
  rows: Row[];
  // totalCount: number;
}

API例: User

UserApiのレスポンスのコアの部分、UserTableDataTableData に ジェネリクスの User type を渡して作っています。rows を持ち、rowsの要素は id, と name, age を持ちます。
ジェネリクスはテーブル特有のものに差し替えて、APIごとに変えて作ります。

export type User = {
  id: string;
  name: string;
  age: number;
};
export type UserTableData = TableData<User>;

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<UserTableData>
) {
  res.status(200).json(getDummy());
}

ダミーデータ例

今回は固定データを返すだけですが、reqgetDummy に渡して、取得件数の指定やフィルタの組み込みをした動的なテータ生成も可能です。

const getDummy = (): UserTableData => {
  return { rows: [...Array(25)].map((_, i) => ({
    id: `id${i + 1}`,
    name: `user${i + 1}`,
    age: 10 + i
  })}
}

Table

テーブルの生成は ReactTable の useTable公式の標準的な使用です。
重要パートは TableProps の ジェネリクスの R です。これがテーブル固有の行の type になります。
useTable に R を渡すことで、続く、TableBodyTableRowTableCell が 型を参照できます。

import { styled, Table as MuiTable } from "@mui/material";

const StyledTable = styled(MuiTable)(() => ({ tableLayout: "fixed" }));

interface TableProps<R extends BaseRow> {
  rows: R[];
  columns: Column<R>[];
}

export default function Table<R extends BaseRow>({
  rows: rs,
  columns,
}: TableProps<R>) {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const data = useMemo(() => rs, []);
  const { getTableProps, headerGroups, rows, prepareRow } = useTable<R>({
    columns,
    data,
  });
  return (
    <StyledTable {...getTableProps()}>
      <TableHeader headerGroups={headerGroups} />
      <TableBody rows={rows} prepareRow={prepareRow} />
    </StyledTable>
  );
}

TableBody

こちら TableBody(表本体) でそのまま rows を各行にわけているだけです。

import { Row } from "react-table";
import { BaseRow } from "../../difinitions/table";

interface Props<R extends BaseRow> {
  rows: Row<R>[];
  prepareRow: (row: Row<R>) => void;
}

export function TableBody<R extends BaseRow>({ rows, prepareRow }: Props<R>) {
  return (
    <MuiTableBody>
      {rows.map((row) => {
        prepareRow(row);
        return <TableBodyRow key={row.id} row={row} />;
      })}
    </MuiTableBody>
  );
}

TableRow

こちらも TableRow(行) でそのまま cells を各列にわけているだけです。

import { Row } from "react-table";
import { BaseRow } from "../../difinitions/table";

interface RowProps<R extends BaseRow> {
  row: Row<R>;
}

function TableBodyRow<R extends BaseRow>({ row }: RowProps<R>) {
  return (
    <MuiTableRow {...row.getRowProps()}>
      {row.cells.map((cell) => {
        const { key } = cell.getCellProps();
        return <TableBodyCell cell={cell} key={key} />;
      })}
    </MuiTableRow>
  );
}

TableCell

セル(列)の表示を行っています。今回はパスしていますが、Column によって Cell によるフォーマットの出し分けも可能です。次回以降の記事で触れていきたい所存です。

import { Cell } from "react-table";
import { BaseRow } from "../../difinitions/table";

interface CellProps<R extends BaseRow> {
  cell: Cell<R, any>;
}

function TableBodyCell<R extends BaseRow>({ cell }: CellProps<R>) {
  return (
    <MuiTableCell {...cell.getCellProps()}>
      {cell.render("Cell")}
    </MuiTableCell>
  );
}

TableHeader

テーブルヘッダーです。こちらも TableBody と同様に、 HeaderRow > HeaderCell と降りていきます。TableBody と近しい構造のため解説は省略します。

import { HeaderGroup } from "react-table";

import { BaseRow } from "../../difinitions/table";

interface Props<R extends BaseRow> {
  headerGroups: HeaderGroup<R>[];
}
interface HeaderRowProps<R extends BaseRow> {
  headerGroup: HeaderGroup<R>;
}
interface HeaderCellProps<R extends BaseRow> {
  column: HeaderGroup<R>;
}

const TableHeaderCell = <R extends BaseRow>({ column }: HeaderCellProps<R>) => {
  return <th {...column.getHeaderProps()}>{column.render("Header")}</th>;
};

const TableHeaderRow = <R extends BaseRow>({
  headerGroup,
}: HeaderRowProps<R>) => {
  return (
    <tr {...headerGroup.getHeaderGroupProps()}>
      {headerGroup.headers.map((column) => (
        <TableHeaderCell column={column} key={column.id} />
      ))}
    </tr>
  );
};

export const TableHeader = <R extends BaseRow>({ headerGroups }: Props<R>) => {
  return (
    <thead>
      {headerGroups.map((headerGroup) => {
        const { key } = headerGroup.getHeaderGroupProps();
        return <TableHeaderRow key={key} headerGroup={headerGroup} />;
      })}
    </thead>
  );
};

ページでテーブルを呼び出す

useUserTable前回記事で解説している hook で、APIで user データを取得する想定です。
columns が テーブルの rowsにそれぞれデータを振り分けています。

import { UserTableData } from "../../api/users";

const columns = [
  { Header: "id", accessor: "id" as const },
  { Header: "名前", accessor: "name" as const },
  { Header: "年齢", accessor: "age" as const },
];

export default function UserTable() {
  const data = useUserTable();
  const content = data ? (
    <Table<UserTableData> rows={data.rows} columns={columns} />
  ) : (
    <p>Loading...</p>
  );
  return <PageBase title={"User"}>{content}</PageBase>;
}

テーブルの描画ができました

おわりに

ジェネリクスのR

APIの時点からテーブルの描画までは、TypeScript の型は目まぐるしく形を変えていくのですが、その中で共通の 行<R> を受け渡していくの美しいですよね。

今後の拡張

次回以降の記事では以下についても書いていきたいなと思っています。

  • 各セルの幅の調整
  • 渡した値の種類からセルの描画を変える( Columns )
  • セルの折りたたみ
  • 行全体にかかわる表現(重要行の色を変えるなど)

Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています

弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!

投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。