Reactでテーブル表示をする際に、いろいろ考えないといけないことが多いです。
ページネーション(Pagination)したいとか、ソート(Sorting)したいとか、フィルタリング(Filtering)したいとか…、いろいろあります。
これら全部を自前で実装するのは大変ですが、Tanstack Tableを使うと簡単に実装できるそうです。
ドキュメント見てTanstack Tableすごそうだなーと思うものの、機能が多すぎてどんなことできるのかいまいちピンときてないんですよね。
なんで、Tanstack Tableをざっと使ってみて具体的にどんなことができるのかを見てみたいと思います。
このブログでは、最終的にこんな感じのテーブルが作れるようになります。
Tanstack Tableとは
Tanstack Tableは、テーブル実装に必要な様々な機能を備えたHeadless UIライブラリです。
テーブル操作に必要なロジック部分とデザイン部分が分離されているため、柔軟にUIをカスタマイズできるのが特徴です。
テーブルの作成(ブログの紹介)
作り方はこっちのブログを参考にしてください。
このブログでは作った後からあれやこれやします。
基本のテーブル
まずは、基本のテーブルです。
useReactTable
でテーブルを作成します。
columns
でカラムを定義し、 data
でデータを定義します。
data
はWebAPIからの非同期な取得を想定して、 useEffect
で非同期にデータを取得して、 useState
でデータを適用しています。
(↓クリックすると埋め込んだStackBlitzでコードとプレビューページが表示されます)
Headless UIなので、スタイルは何もないです。
これだけシンプルなテーブルならTanstack Tableを使わず、 <table>
タグで作ればいいと思います。
実際の開発で、こんなシンプルなテーブルを使うことはほとんど無いですね。
スタイルをつける
あまりにも簡素すぎるので、CSSでスタイルをつけます。
CSSは styles.css
として別ファイルから読み込んでいるので、次のCSSを styles.css
に追加してスタイルをつけます。
styles.css
table {
border-collapse: collapse;
border: 2px solid black;
padding: 1px;
}
th {
border: 1px dashed lightgray;
border-bottom: 1px solid black;
padding: 4px;
background: lavender;
}
td {
border: 1px dashed lightgray;
padding: 4px;
}
ちょっとだけ見栄えがよくなりました。
データの表示を変える
テーブルのデータは、表示を変えたいことがよくあります。
例えば、何らかのフラグ真偽値を 有効 と 無効 に変えたいとか、日付を yyyy/MM/dd に変えたいとか。
そういう時は、 ColumnDef
の cell
を変えると、セルの表示を自分の好きなように変えることができます。
今回は、UNIXタイムスタンプ(ミリ秒)な登録日時(createdAt)の表示を、 yyyy/MM/dd HH:mm に変えてみます。
時間の取り扱いを自前で作るのがめんどくさいので、 date-fns
を使います。
こんな感じにすれば、 date-fns
を使って日付をフォーマットできます。
import { format } from 'date-fns';
import ja from 'date-fns/locale/ja';
const displayDate = format(new Date(user.createdAt), 'yyyy/MM/dd HH:mm', {
locale: ja,
})
次のように、テーブルの ColumnDef
の cell
に表示したいReactコンポーネントを返す関数を定義することで、セルの表示を変えられます。
src/column.tsx
export const columns: ColumnDef<User>[] = [
...
{
accessorKey: 'createdAt',
header: '登録日時(UNIX)',
cell: ({ row }) => {
const user = row.original;
return format(new Date(user.createdAt), 'yyyy/MM/dd HH:mm', {
locale: ja,
});
},
},
...
];
そうすると、こんな感じで日付が表示されます。
ボタンの追加
テーブルのデータごとに 編集ボタン みたいな、何かしらのアクションを追加したいことがあります。
今回は、 編集ボタン を追加して、クリックしたらアラートを表示するようにしてみます。
アイコンは自分で作るのがめんどくさいので、 react-icons
を使います。
次のように、テーブルの ColumnDef
にカラム定義を追加して、 cell
に表示したいアイコンとアクションを定義することで、行にアクションを追加できます。
src/column.tsx
import {
TiEdit,
} from "react-icons/ti";
export const columns: ColumnDef<User>[] = [
{
id: "actions",
cell: ({ row }) => {
const user = row.original;
return (
<div
style={{ cursor: "pointer" }}
onClick={() =>
alert(`${user.id}:${user.email}の編集ボタンがクリックされました。`)
}
>
<TiEdit />
</div>
);
},
},
...
];
そうすると、こんな感じで 編集ボタン がテーブルに表示されます。
ページネーション(Pagination)
表示するデータが多い場合、一度に全て表示するのではなく、ページごとに決まった件数を表示(ページネーション)したいです。
Tanstack Tableを使うとページネーションが簡単に実装できます。
useReactTableの引数に getPaginationRowModel
を追加するだけです。
src/App.tsx
const table = useReactTable<User>({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
デフォルトでは、ページごとに10件表示され、最初のページが表示されます。
useReactTable
の initialState
で、ページごとの件数と、表示するページ数の初期値が設定できます。
src/App.tsx
const initialPageIndex = 0;
const initialPageSize = 10;
const table = useReactTable<User>({
columns,
data,
initialState: {
pagination: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
- Table APIs - initialState | TanStack Table Docs
- Pagination APIs - PaginationTableState | TanStack Table Docs
現在のページサイズやページ番号を取得するには、 table.getState().pagination
を参照します。
- Table APIs - getState() | TanStack Table Docs
- Pagination APIs - PaginationTableState | TanStack Table Docs
ページサイズを変更するには、 table.setPageSize
を使います。
ページ番号を変更するには、 table.setPageIndex
を使います。
その他、ページネーションに関するこれらの関数があります。(一部抜粋)
- nextPage, previousPage: 次のページ、前のページに移動
- canNextPage, canPreviousPage: 次のページ、前のページに移動できるかどうかを判定して真偽値を返す
これらの関数を使うと、こんな感じのページネーション対応のテーブルが簡単に作れます。
ついでに table.getState().pagination
を使って、ページ番号やページサイズを表示してみます。
ソート(Sorting)
テーブルはユーザーにソートさせたい場合もあります。
Tanstack Tableを使うとソートも簡単に実装できます。
useReactTable
の引数に getSortedRowModel
を追加するだけで、内部的にはテーブルがソート可能な状態になります。
src/App.tsx
const table = useReactTable<User>({
columns,
data,
initialState: {
pagination: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
},
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
例えば初期状態として、 id
の降順でソートされた状態にしたい場合は、useReactTable
に次のように追加することで実現できます。
const table = useReactTable<User>({
columns,
data,
initialState: {
pagination: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
},
sorting: [{ id: "id", desc: true }],
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
一応ソートが実現できました。 が、そうではなくて、ユーザーの操作でソートできるようにしたいです。
その場合、自分でUIを作る必要があります。 今回は、テーブルのヘッダーにこういうアイコンを表示してみましょう。
なんとなくソートできるような感じがしません?
ソートしたら、こういうアイコンを表示して、昇順、降順でソートされていることを明示するようにしてみます。
方針は決めたので、これを実装してみます。
テーブルヘッダーの表示を変えたい場合は、 ColumnDef
の header
を変えるとヘッダーの表示が変えられます。
src/column.tsx
import {
TiArrowSortedDown,
TiArrowSortedUp,
TiArrowUnsorted,
} from "react-icons/ti";
const getSortIcon = (sortDirection: false | SortDirection): JSX.Element => {
switch (sortDirection) {
case "asc":
return <TiArrowSortedUp />;
case "desc":
return <TiArrowSortedDown />;
default:
return <TiArrowUnsorted />;
}
};
export const columns: ColumnDef<User>[] = [
{
accessorKey: "id",
header: ({ column }) => {
return (
<div
style={{ flex: "auto", alignItems: "center", cursor: "pointer"}}
onClick={column.getToggleSortingHandler()}
>
ID{getSortIcon(column.getIsSorted())}
</div>
);
},
},
...
];
column.getToggleSortingHandler
は、ソートの状態を 降順 、 昇順 、 ソート無し と替えるための関数です。
これをヘッダーをクリックしたときのイベントハンドラに設定します。
column.getIsSorted
は、そのカラムがソートされているかどうかを返す関数です。これを使って、ソートされている場合にアイコンを変えるようにします。
これらの関数を使うと、こんな感じのソート対応のテーブルが簡単に作れます。
ついでに table.getState().sorting
を使って、ソートの状態を表示してみます。
フィルタリング(Filtering)
テーブルのデータを検索して絞り込みたい場合もあると思います。
Tanstack Tableを使うとフィルタリングも簡単に実装できます。
useReactTable
の引数に getFilteredRowModel
を追加するだけで、内部的にはテーブルがフィルタリング可能な状態になります。
src/App.tsx
const table = useReactTable<User>({
columns,
data,
initialState: {
pagination: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
},
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
例のごとく、このままだとユーザーがフィルタリングできるUIを表示できるわけではありません。
ユーザーの操作でフィルタリングできるようにするには、自分でUIを作る必要があります。
フィルタリングしたい値のセットは、 column
の setFilterValue
を使うとできます。
セットした値は、 column
の getFilterValue
で確認できます。
カラム自体は、 table
の getColumn
を使って取得できます。
これらの関数を使うと、こんな感じでフィルタリング機能が簡単に作れます。
ついでに table.getState().columnFilters
を使って、フィルタリングの状態を表示してみます。
グローバルフィルタリング(Global Filtering)
1項目だけでなく、複数の項目を横断的に検索したい場合もあります。
そんな時は、グローバルフィルタリングを使います。
グローバルフィルタリングの値のセットは、 table
の setGlobalFilter
を使ってできます。
セットした値は、 table
の getState().globalFilter
で取得できます。
これで、全項目に対して横断的に検索できるようになります。
だけども、実際には検索対象外としたい項目もあると思います。
今回で言えば、 登録日時 は検索対象外としたいです。 ID で検索しようと思ってるのに、 登録日時 の日時まで検索対象になると鬱陶しそうです。
そういう時は、次のようにテーブルの ColumnDef
の enableGlobalFilter
を false
にしておくと、グローバルフィルタリングの対象外になります。
src/column.tsx
export const columns: ColumnDef<User>[] = [
...
{
accessorKey: "createdAt",
header: sortableHeader('登録日時(UNIX)'),
cell: ({ row }) => {
const user = row.original;
return format(new Date(user.createdAt), "yyyy/MM/dd HH:mm", {
locale: ja,
});
},
enableGlobalFilter: false,
},
...
];
これらの関数を使うと、こんな感じでグローバルフィルタリング機能が簡単に作れます。
ついでに table.getState().globalFilter
を使って、フィルタリングの状態を表示してみます。
検索対象に日本語が入ってくると、カタカナ、ひらがな、全角、半角などいろいろな表記があるので、それらを考慮して検索したい気がしてきますが、今回は割愛します。
フロントで日本語のあいまい検索って簡単にできるんですかね?誰か教えてください。
フィルタリングの拡張(Filtering+)
単純な文字列検索だけでなく、複雑な検索をしたい場合もあります。
例えば、今回は 登録日時 があるので、特定の日付の範囲のデータを検索したいとか、そんな場合を考えてみます。
フィルタリングのロジックは、デフォルトだとTanstack Tableがいい感じにやってくれますが、自分でフィルタリング条件を定義したい場合は、 ColumnDef
の filterFn
でフィルタリングのロジックを定義できます。
次の FilterFn
の定義からわかる通り、 filterValue
の型が any
で何でも入れられます。
export type FilterFn<TData extends AnyData> = {
(
row: Row<TData>,
columnId: string,
filterValue: any,
addMeta: (meta: any) => void
): boolean
resolveFilterValue?: TransformFilterValueFn<TData>
autoRemove?: ColumnFilterAutoRemoveTestFn<TData>
addMeta?: (meta?: any) => void
}
と、いうことは column
の setFilterValue
入れる値は 文字列 じゃなくても何でもいいわけです。
そういうわけで、 filterValue
に日付の範囲を入れてフィルタリングできるようなロジックにしてみましょう。
filterValue
に {from?: string, to?: string}
な型で、日付の書式は yyyy-MM-dd
なデータが入ってくることを前提とします。後々、日付の範囲を入れるUIを作るときに、この書式となるようにします。
from
が設定されている場合は from
以降のデータを、 to
が設定されている場合は to
以前のデータを絞り込むようなロジックはこんな形でできます。
src/column.tsx
import { parse } from "date-fns";
...
filterFn: (row, _, filterValue) => {
const { from, to } = filterValue as { from?: string; to?: string };
const createdAt = row?.original?.createdAt;
return (
(!from ||
parse(from, "yyyy-MM-dd", new Date()).getTime() <= createdAt) &&
(!to || createdAt <= parse(to, "yyyy-MM-dd", new Date()).getTime())
);
},
...
先ほど、 column
の setFilterValue
を使うと、フィルタリングの値をセットできると書きました。
この setFilterValue
の引数は、値だけでなく、古い値を引数にした関数を使うことができます。
- Filter APIs - setFilterValue() | TanStack Table Docs
- table/packages/table-core/src/types.ts at main · TanStack/table
つまり、setFilterValue
の引数に次のようなupdate関数をセットすることで、 filterValue
の一部の値だけ更新できます。
const updater = (
old: { from?: string; to?: string }
) => {
return {
...old,
from: "newFromValue",
};
}
これらの関数を使うと、こんな感じで日付範囲のフィルタリング機能も作れます。
終わりに
今回は、Tanstack Tableを使ったテーブルの実装をやってみました。
テーブルの実装に必要な機能が一通り揃っていて多機能でめっちゃ便利だなと思った反面、ドキュメントを読むのがちょっと難しいなと思ったので、具体例多めでざっと使い方を紹介してみました。
Exampleが豊富なので、コードを読んで理解するのが一番早そうです。