TanStack Table の 設計とTips
自己紹介
2022年に中途入社した鈴木海斗です。フロントエンドやアルゴリズム開発を中心におこなっています。本記事ではフロントエンド開発でテーブルを作る時に利用可能なライブラリの一つである TanStack Table の基本的な使い方を紹介していきます。
対象読者
フロントエンド開発で TanStack Table を導入しようとしている方。
ドキュメントのゴール
TanStack Table に関する以下の項目について基本的な使い方を説明します。
- テーブルの表示
- ソート
- 行の更新、追加
- カラムの表示順、ピン
TanStack Table とは
TanStack Table は TypeScript/JavaScript、Angular、Lit、React、Vue、Solid、Qwik、Svelte 向けのテーブルを構築するための Headless UI ライブラリです。Headless UI とはスタイルを提供せず、ロジックや状態のみを提供することで独自のUIを作成可能なものを指します。Headless UI ライブラリに対してデザインも含めて事前構築されたコンポーネントを提供する UIコンポーネントライブラリがあり、例えば TanStack Table を元にデザインを当てたライブラリとして Material React Table などが存在します。Headless UI であることで自由にスタイリングを行うことが可能になる一方、その分の工数が必要になるため用途に応じて選択することになります。
環境
本記事では主要ライブラリが以下のバージョンでの Next.js を用いた実装例を紹介します。TypeScript など他に利用しているライブラリがありますが記載は割愛しています。
- next: 13.4.7
- react: 18.2.0
- @tanstack/react-table: 8.9.3
テーブルの表示
ここからは React を利用した際における TanStack Table を用いた実装例を紹介していきます。
TanStack Table は Headless UI ライブラリなので、ロジック部分の実装に加えて UI に関する実装を CSS や他の UI コンポーネントライブラリを用いて別でコーディングする必要があります。
例として、hoge
と fuga
をカラムに持つ基本的なテーブルの実装例を紹介します。ロジックを /app/page.tsx
、UI を /components/ShowTable.tsx
に実装するものとします。
以下のコードが /app/page.tsx
の実装です。
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';
// テーブルデータの型を定義
type TestTable = {
hoge: string;
fuga: number;
}
// createColumnHelper を利用してカラム定義を作成
const columnHelper = createColumnHelper<TestTable>();
const testTableColumnDefs = [
columnHelper.accessor((row) => row.hoge, {
id: 'hoge',
header: 'hoge',
}),
columnHelper.accessor((row) => row.fuga, {
id: 'fuga',
header: 'fuga',
}),
];
export default function Page() {
const [data, setData] = useState<TestTable[]>([
{ hoge: 'hoge1', fuga: 0 },
{ hoge: 'hoge2', fuga: 1 },
{ hoge: 'hoge3', fuga: 2 },
]);
// useReactTable 呼び出し
const table = useReactTable<TestTable>({
columns: testTableColumnDefs,
data: data,
getCoreRowModel: getCoreRowModel(),
})
return <ShowTable table={table} />
}
流れを大まかに説明すると以下のようになります。
- テーブルデータの型を定義
createColumnHelper
を利用してカラム定義を作成useReactTable
呼び出し
最低限のUI部分のコンポーネント実装例が以下のコードです。
import {
Cell,
Header,
Table, flexRender,
} from '@tanstack/react-table';
const ShowHeader = ({
table,
}:{
table: Table<any>;
}) => {
return (
<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>
)
}
const ShowBody = ({
table,
}:{
table: Table<any>;
}) => {
return (
<tbody>
{table.getRowModel().rows.map((row, index) => {
const tableCell = (cell: Cell<any, unknown>, cellIndex?: number) => (
<td key={cell.column.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
return (
<tr
key={index}
style={{ textAlign: 'center' }}
>
<>{row.getVisibleCells().map(tableCell)}</>
</tr>
);
})}
</tbody>
)
}
export const ShowTable = ({
table
}:{
table: Table<any>;
}
) => {
return (
<div>
<main>
<table>
<ShowHeader table={table} />
<ShowBody table={table} />
</table>
</main>
</div>
);
}
Table
を引数とするこのコンポーネントの実装によってUIのカスタマイズが可能になります。
上記実装を行ったものが以下の画像です。
ソート
useReactTable
の引数に getSortedRowModel()
を渡すことで、クライアント側でのソート機能を実現できます。
ソートの状態は SortingState
で表し、配列の先頭の要素から順にキーとしてソートが行われます。
また、比較関数をカラム定義で sortingFn
に渡すことで変更することができ、デフォルトで用意されたものがいくつかありますが自作することも可能です。
hoge
カラムにデフォルト比較関数の alphanumeric
、fuga
カラムに undefined が混ざっている時の挙動を定義した自作の比較関数で、fuga
を第一ソートキーで降順、hoge
を第二ソートキーで昇順としたソート機能を持たせる実装例を以下に示します。
import { SortingState, createColumnHelper, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';
type TestTable = {
hoge: string;
fuga: number | undefined;
}
const columnHelper = createColumnHelper<TestTable>();
const testTableColumnDefs = [
columnHelper.accessor((row) => row.hoge, {
id: 'hoge',
header: 'hoge',
sortingFn: 'alphanumeric' // 英数字用の比較関数
}),
columnHelper.accessor((row) => row.fuga, {
id: 'fuga',
header: 'fuga',
sortingFn: (a, b, columnId) => { // undefinedを最も大きいとする自作比較関数
if(a.original.fuga == undefined && b.original.fuga == undefined){
return 0;
}
if(a.original.fuga == undefined){
return 1;
}
if(b.original.fuga == undefined){
return -1;
}
return a.original.fuga - b.original.fuga;
}
}),
];
export default function Page() {
const [data, setData] = useState<TestTable[]>([
{ hoge: '1', fuga: 0 },
{ hoge: '3', fuga: 2 },
{ hoge: '2', fuga: 2 },
{ hoge: '4', fuga: undefined },
]);
// 第一ソートキーをfugaで降順、第二ソートキーをhogeで昇順としている
const [sorting, setSorting] = useState<SortingState>([{ id: 'fuga', desc: true}, { id: 'hoge', desc: false}]);
const table = useReactTable<TestTable>({
columns: testTableColumnDefs,
data: data,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), // クライアント側でのソートを行いたい場合引数に渡す
state: {
sorting: sorting,
},
onSortingChange: setSorting,
})
return <ShowTable table={table} />
}
上記実装を行ったものが以下の画像です。
参考: https://tanstack.com/table/latest/docs/guide/sorting#sorting-guide
行の更新、追加
Cell への入力に基づいてテーブルデータを動的に更新したいことがあると思います。
TanStack Table ではuseReactTable
の引数である meta にメソッドを渡すことでCell コンポーネント内から cell.table.options.meta
としてアクセスできます。
これを利用することで行の更新や追加を行うことができます。
以下のコードは、行の更新、追加、複製機能を持ったテーブルの実装例です。
import { RowData, createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';
// metaに渡す型を宣言
declare module '@tanstack/react-table' {
interface TableMeta<TData extends RowData> {
addRow: (row: TData) => void;
updateRow: (index: number, row: TData) => void;
}
}
type TestTable = {
hoge: string;
fuga: number;
}
const columnHelper = createColumnHelper<TestTable>();
const testTableColumnDefs = [
columnHelper.accessor((row) => row.hoge, {
id: 'hoge',
header: 'hoge',
}),
columnHelper.accessor((row) => row.fuga, {
id: 'fuga',
header: 'fuga',
}),
columnHelper.display({ // 同じ行を更新するカラム
id: 'update',
header: '更新',
cell: (cell) => {
return <button onClick={() => {
cell.table.options.meta?.updateRow(cell.row.index, { hoge: 'updated', fuga: cell.row.getValue('fuga') as number + 1});
}
}>更新</button>
}
}),
columnHelper.display({ // 複製を行うカラム
id: 'duplicate',
header: '複製',
cell: (cell) => {
return <button onClick={() => {
cell.table.options.meta?.addRow(cell.row.original);
}
}>複製</button>
}
}),
];
export default function Page() {
const [data, setData] = useState<TestTable[]>([
{ hoge: 'hoge', fuga: 0 },
{ hoge: 'hoge', fuga: 1 },
{ hoge: 'hoge', fuga: 2 },
]);
const table = useReactTable<TestTable>({
columns: testTableColumnDefs,
data: data,
getCoreRowModel: getCoreRowModel(),
meta: { // talble.options.meta からアクセスしたいものを渡す
addRow: (row: TestTable) => {
setData([...data, row]);
},
updateRow: (index: number, row: TestTable) => {
setData(data.map((d, i) => i === index ? row : d));
}
}
})
return <>
<ShowTable table={table} />
<button onClick={() => { // 新しい行を追加するボタン
table.options.meta?.addRow({ hoge: 'new', fuga: table.getRowModel().rows.length});
}
}>追加</button>
</>
}
上記実装を行ったものが以下の画像です。
参考: https://tanstack.com/table/latest/docs/api/framework/react/examples/editable-data
カラムの表示順、ピン
例えばユーザーごとにカラムの表示順を変えたいときや、一部のカラムをピンしたい時があります。
このような時にも TanStack Table の機能で対応することが可能です。
表示順やピンのステータスを useState
で宣言し、それを useReactTable
の引数に渡すことで、変更することができます。
以下がデフォルトでカラムの表示順を設定し、ヘッダーに動的にピンを設定できるUIを追加した実装例です。
import { Column, ColumnPinningState, createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';
import { RiPushpin2Fill } from "react-icons/ri";
type TestTable = {
hoge: string;
fuga: number;
}
const columnHelper = createColumnHelper<TestTable>();
// ピンをオンオフできるボタン
const PinToggleButton = <T,>({ column }: { column: Column<T, unknown>}) => {
if(!column.getCanPin()){
return null;
}
return <button onClick={() => {
if(column.getIsPinned() === 'left'){
column.pin(false);
}else{
column.pin('left');
}
}}><RiPushpin2Fill /></button>
}
const testTableColumnDefs = [
columnHelper.accessor((row) => row.hoge, {
id: 'hoge',
header: (header) => { // ヘッダーにピンを設定できるボタンを表示
return <>
hoge
<PinToggleButton column={header.column} />
</>
},
}),
columnHelper.accessor((row) => row.fuga, {
id: 'fuga',
header: (header) => {
return <>
fuga
<PinToggleButton column={header.column} />
</>
}
}),
];
export default function Page() {
const [data, setData] = useState<TestTable[]>([
{ hoge: 'hoge1', fuga: 0 },
{ hoge: 'hoge2', fuga: 1 },
{ hoge: 'hoge3', fuga: 2 },
]);
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({ left: [], right: []}); // ピンの設定
const [columnOrder, setColumnOrder] = useState<string[]>(['hoge', 'fuga']); // カラムの表示順設定
// state と onColumnPinningChange、onColumnOrderChange に必要なものを渡す
const table = useReactTable<TestTable>({
columns: testTableColumnDefs,
data: data,
getCoreRowModel: getCoreRowModel(),
state: {
columnPinning: columnPinning,
columnOrder: columnOrder,
},
onColumnPinningChange: setColumnPinning,
onColumnOrderChange: setColumnOrder,
})
return <>
<ShowTable table={table} />
<button onClick={() => { // 表示順を後から変更するボタン
setColumnOrder(['fuga', 'hoge']);
}
}>Change Column Order</button>
</>
}
上記実装を行ったものが以下の画像です。
参考: https://tanstack.com/table/latest/docs/guide/column-ordering#column-ordering-guide https://tanstack.com/table/latest/docs/guide/column-pinning#column-pinning-guide
その他
テーブルデータのうちいくつかのカラムについては中身が空であるというケースはよくあると思います。
そのようなデータをどのように表示するかは acccesor
や cell
にカラムごとに定義することもできますが、 useReactTable
の引数に renderFallbackValue
を設定することでデータが空の時の表示を一律に設定することが可能です。
以下がデータに undefined が混ざっている時に renderFallbackValue
を設定した実装例です。
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';
type TestTable = {
hoge: string;
fuga: number | undefined; // undefinedが入ることがある
}
const columnHelper = createColumnHelper<TestTable>();
const testTableColumnDefs = [
columnHelper.accessor((row) => row.hoge, {
id: 'hoge',
header: 'hoge',
}),
columnHelper.accessor((row) => row.fuga, {
id: 'fuga',
header: 'fuga',
}),
];
export default function Page() {
const [data, setData] = useState<TestTable[]>([
{ hoge: 'hoge1', fuga: undefined },
{ hoge: 'hoge2', fuga: 1 },
{ hoge: 'hoge3', fuga: undefined },
]);
const table = useReactTable<TestTable>({
columns: testTableColumnDefs,
data: data,
getCoreRowModel: getCoreRowModel(),
renderFallbackValue: '-', // データがない時デフォルトで表示したい値を設定
})
return <ShowTable table={table} />
}
上記実装を行ったものが以下の画像です。
失敗談
プロジェクトで使っているテーブルに共通して行いたい設定などがある場合に、useReactTable
をラップしたコンポーネントを作ることになると思います。
TanStack Tableではページネーションを行うための機能もあり、ラップしたコンポーネント内でページネーションのデフォルトサイズを決めるような実装をしていたことにより、ページネーションを想定していない他のテーブルで表示される行数に制限が発生するというバグを生んでしまったことがあります。
どこまでの設定をデフォルトで行うかという点には注意が必要です。
まとめ
TanStack Table の機能を具体例と併せていくつか紹介していきました。 実際にプロジェクトで使用してソートやフィルターなど様々な機能を簡単に実装できると感じています。 ここで紹介しきれていない機能もまだまだたくさんあるので、ぜひ公式ドキュメントも合わせて TanStack Table を使ってみてください。
参考文献
エムシーデジタルでは、技術力向上のためのイベントや勉強会なども定期的に実施しています。もしエムシーデジタルでのキャリアに興味を持っていただいた方がいらっしゃいましたら、まずはカジュアルな面談から実施することも可能です。お気軽にお声掛けください!
採用情報や面談申込みはこちらから