ReactでTanstack Tableを使ったテーブル実装を試してみた

2023.01.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。データアナリティクス事業本部 サービスソリューション部の北川です。

チーム内でTanstack Tableを使用する機会がありました。自分はまだ使ってませんが、今後使う可能性や、キャッチアップも兼ねて簡単にAPIを触ってみました。

Tanstack Tableとは

Tanstack Tableは、テーブルの作成を容易に実装できるヘッドレスUIライブラリになります。ヘッドレスなので柔軟性が高いのが特徴です。React Tableから移行されたライブラリであり、TypeScriptへ置き換わっているのが大きな変更点になります。

フィルタリングや、ソート、グルーピングなど機能も豊富です。今回は、その中でもページネーションのAPIを触ってみました。

試してみた

今回は、Next.jsを使用して、プロジェクトを作成します。

$ npx create-next-app sample-table --ts

プロジェクトに移動し、Tanstack Tableをインストールします。

$ yarn add @tanstack/react-table

テーブル作成

適当なファイルを作成して、テーブルの中身にあたるDATAと、列にあたるCOLUMNSを定義します。columnsのHeaderにはテーブルのヘッダーの値、accessorにはオブジェクトのキーを指定します。

libs/data.ts

import { ColumnDef } from "@tanstack/react-table";

type TData = {
  col1: string;
  col2: string;
  col3: number;
};

export const DATA: TData[] = [
  {
    col1: "Hello",
    col2: "World",
    col3: 1234,
  },
  {
    col1: "react-table",
    col2: "rocks",
    col3: 5678,
  },
  {
    col1: "whatever",
    col2: "you want",
    col3: 9012,
  },
];

export const COLUMNS: ColumnDef<any>[] = [
  {
    header: "Column 1",
    accessorKey: "col1",
  },
  {
    header: "Column 2",
    accessorKey: "col2",
  },
  {
    header: "Column 3",
    accessorKey: "col3",
  },
];

useReactTableフックを使用してテーブルをインスタンス化します。定義したDATAとCOLUMNSを、useReactTableに渡します。

pages/index.tsxを編集します。useMemoを使用して、データが不要に再レンダリングするのを防ぎます。

pages/index.tsx

import type { NextPage } from "next";
import styles from "../styles/Home.module.css";
import {
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { COLUMNS, DATA } from "../libs/data";
import { useMemo } from "react";

const Home: NextPage = () => {
  const columns = useMemo(() => COLUMNS, []);
  const data = useMemo(() => DATA, []);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <table>
          <thead>
            {table.getHeaderGroups().map((headerGroup) => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <th key={header.id} colSpan={header.colSpan}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody>
            {table.getRowModel().rows.map((row) => {
              return (
                <tr key={row.id}>
                  {row.getVisibleCells().map((cell) => {
                    return (
                      <td key={cell.id}>
                        {flexRender(
                          cell.column.columnDef.cell,
                          cell.getContext()
                        )}
                      </td>
                    );
                  })}
                </tr>
              );
            })}
          </tbody>
        </table>
      </main>
      <p>{table.getRowModel().rows.length} Rows</p>
    </div>
  );
};

export default Home;

スタイリング

ヘッドレスなので、スタイルは完全にこちら側で制御します。今回は試すだけなので、global.cssファイルにcssを追記しました。

styles.global.css

table {
  border: 1px solid lightgray;
  padding: 1px;
}

tbody {
  border-bottom: 1px solid lightgray;
}

th {
  border-bottom: 1px solid lightgray;
  padding: 4px;
}

td {
  padding: 4px;
}

tfoot {
  color: gray;
}

tfoot th {
  font-weight: normal;
}

ページネーション

ページネーションの実装には、getPaginationRowModelを使用します。

const table = useReactTable({
  data: data,
  columns: columns,
  getCoreRowModel: getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
});

ページネーションの機能だけでも、多くのメソッドが用意されています(以下は一部)。

  • getCanPreviousPage, getCanNextPage : 前のページ、次のページに移動できるかどうか真偽値を返す

  • nextPage、previousPage : 次のページに移動、前のページに移動する

  • setPageIndex : 指定のインデックスまで移動する

  • setPageSize : テーブルの表示領域を指定する

  • getPageCount : 全体のページ数を返す

pages/index.tsxを以下のように変更してみました。今回はuseSWRと、JSONPlaceholderを使用して、仮データを用意しています。

pages/index.tsx

import type { NextPage } from "next";
import styles from "../styles/Home.module.css";
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getPaginationRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { useMemo } from "react";
import useSWR from "swr";

const fetcher = (args: string) => fetch(args).then((res) => res.json());

type TData = {
  userId: number;
  id: number;
  title: string;
  body: string;
};

const Home: NextPage = () => {
  const { data, isLoading } = useSWR(
    "https://jsonplaceholder.typicode.com/posts",
    fetcher
  );

  const columns = useMemo<ColumnDef<TData>[]>(
    () => [
      {
        header: "userId",
        accessorKey: "userId",
      },
      {
        header: "Id",
        accessorKey: "id",
      },
      {
        header: "Title",
        accessorKey: "title",
      },
    ],
    []
  );

  const table = useReactTable({
    data: data,
    columns: columns,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    debugTable: true,
  });

  if (!table || isLoading) return <></>;
  return (
    <main className={styles.main}>
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} colSpan={header.colSpan}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => {
            return (
              <tr key={row.id}>
                {row.getVisibleCells().map((cell) => {
                  return (
                    <td key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
      <div style={{ margin: "5px" }}>
        <span>Page</span>
        <strong>
          {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
        </strong>
      </div>
      <div>
        <button
          onClick={() => table.setPageIndex(0)}
          disabled={!table.getCanPreviousPage()}
        >
          {"<<"}
        </button>
        <button
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          {"<"}
        </button>
        <button
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          {">"}
        </button>
        <button
          onClick={() => table.setPageIndex(table.getPageCount() - 1)}
          disabled={!table.getCanNextPage()}
        >
          {">>"}
        </button>
      </div>
      <select
        style={{ margin: "10px" }}
        value={table.getState().pagination.pageSize}
        onChange={(e) => {
          table.setPageSize(Number(e.target.value));
        }}
      >
        {[10, 20, 30, 40, 50].map((pageSize) => (
          <option key={pageSize} value={pageSize}>
            Show {pageSize}
          </option>
        ))}
      </select>
      <div>{table.getRowModel().rows.length} Rows</div>
    </main>
  );
};

export default Home;

ページネーション機能も簡単に実装することができました。他にも、指定のページまで飛ぶためのメソッドなどが用意されています。

まとめ

今回は、Tanstack Tableを使用したテーブルの実装を試してみました。ドキュメントには、それぞれの機能を使った実装例ののsandboxが用意されているので、コードを通して使い方を学べるのはありがたかったです。

ではまた。