最近、TanStack Query, TanStack Router, TanStack TableなどのTanStackというライブラリーの記事を良く目にするようになってきました。
TanStack Queryは、以前はReact Queryと呼ばれていた非同期通信を簡単に書けるライブラリーで、知ってる人は多いと思います。
What Is TanStackというブログによるとTanはTypeScript, Next.js, and Reactに由来する名前だと書かれています(Next.jsの色は薄いような気がしますが・・・)。 TanStackのホームページには以下の画像のように High-quality open-source software for web developers. と書かれています。
今回はTanStack Tableに付いて書きます。Headless UIって何なんでしょうか?
準備
TanStackのGETTING STARTEDを読んでみたあのですが、いまひとつ使い方がわからなかったので、ブログ等の記事をよみながら使い方をしらべました。最終的にはTanStackのEXAMPLESのコードが役立ちました(CodeSandboxのリンクをクリックして動かすのが良いと思います)。
今回はバックエンドにjson-serverを使いました、これはJSON形式のデータファイルを準備するだけでREST APIのバックエンドが簡単に作れます。
データのはFree Data Sets & Dataset Samplesで見つけたワイン(正確にはワインテースティング)のデータを使いました。🍷
Reactの開発環境はViteです。
シンプルなテーブル
まずは、シンプルなサンプルコードです。バックエンドからTanStack Queryでデータを取得し、TanStack Tableに表示しています。
コードは以下のようになります
- ① CSSは最低限のものを準備しました
- ② 表示するデータの定義、Free Data Sets & Dataset Samplesで見つけたデータには多数の項目がありましたが最低限にしました
- ③ テーブルの列(カラム)定義、ここではシンプルですがデータと無関係のカラムやグループ、表示のフォーマットなど色々な定義が出来ます
- ④ TanStack Queryを使ってバックエンドから100件のデータを取得しています
- TanStack Queryを使う事でデータやローディング中のステート管理(
useState
)や取得のライフサイクルuseEffect
のコードは不要になります
- TanStack Queryを使う事でデータやローディング中のステート管理(
- ⑤ ここでテーブル管理用の
table
オブジェクトが作られますgetCoreRowModel:
にデータ管理用モデルを指定します、ここでは基本的なgetCoreRowModel
を指定しています。詳細はRow Models Guideを見てください
- ⑥ 表示コード
- ローディング中は
Loading...
を表示します - 一見複雑そうですが、読んでみると普通のコードですね
- ローディング中は
- ⑦
table.getHeaderGroups()
でヘッダーの情報を取得します- 列定義にグループがある場合はグループ名表示は複数行になります
- ⑧ テーブルの要素データの表示は
flexRender()
ユーティリティ関数を使います - ⑨
getRowModel()
でテーブルの全行データを取得します - ⑩
getVisibleCells()
で行内の要素データを取得します
import { ColumnDef } from '@tanstack/react-table';
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useQuery } from '@tanstack/react-query';
import './App.css'; // ← ①
type Wine = { // ← ②
id: number,
country: string,
variety: string,
winery: string,
title: string
};
const columns: ColumnDef<Wine>[] = [ // ← ③
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'country', header: '国' },
{ accessorKey: 'variety', header: '品種' },
{ accessorKey: 'winery', header: 'ワイナリー' },
{ accessorKey: 'title', header: '名前' },
];
export default function App() {
const { data, isLoading } = useQuery({ // ← ④
queryKey: ['wines'],
queryFn: async () => {
const res = await fetch('http://localhost:3030/wines?_limit=100');
return res.json();
}
});
const table = useReactTable<Wine>({ // ← ⑤
columns,
data,
getCoreRowModel: getCoreRowModel(),
});
if (isLoading) { // ← ⑥
return <p>Loading...</p>;
} else {
return (
<>
<h1>Wine</h1>
<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>
</>
)
}
}
Headless UIとは?
上のコードでわかるように、TanStack TableにはUI要素は含んでいません。テーブルにはソートやページング、フィルタリング、カラムの並び替えなど多数の機能が要求されますが、TanStack Tableは、高機能なテーブルを実現するためのデータ管理やAPIを提供してくれるライブラリーです。
Reactのメリットとして多数の便利なコンポーネントがOSSまたは有料で公開されている事もあります。awesome-react-componentsのTableを見ると多数のテーブル用のコンポーネントがあります。
ただし、開発しているプロジェクトがスタイリングにTailwind CSSを採用している場合はMaterial UIベースのテーブル・コンポーネントは採用できませんよね。
そのような意味で、Heaadless UIでテーブル表示のベースを作り、UIは自由に選べるのはとてもありがたいですよね。
Client-Side Pagination
シンプルなテーブルでは、あまりTanStack Tableのありがたさが感じられないと思うので、テーブルにページング機能を追加してみましょう。
まずは全データをクライアント側に取得してから、クライアント(JavaScript)だけでページングするサンプルです。
コードは少ししか増えません
- ① ページング用のステートpaginationを定義
- 開始ページ(pageIndex)、ページの行数(pageSize)のオブジェクトです
- ② データ取得部分は同じです
- ③ テーブル管理用の
table
オブジェクトの作成 - ④ デバッグ指定、デバッグ情報がconsoleに表示されます
- ⑤ getPaginationRowModelにページネーション用のモデルを指定
- ⑥ ページ切り替え用関数の設定、paginationステートの設定関数を指定
- ⑦ テーブルに関連するステートを設定
- ⑧ ページを戻すボタンの処理
table.previousPage()
を呼び出す事でpaginationステートを変更します- また0ページ以前に行かないように
table.getCanPreviousPage()
関数の値でボタンをon/offしています
- ⑨
現在ページ / 全ページ
の表示- 現在のページはpaginationステートから取得
- 全ページ数は
table.getPageCount()
で取得
- ⑩ ページを進めるボタンの処理、⑧と同様です
import { useState } from 'react';
import { ColumnDef, getPaginationRowModel } from '@tanstack/react-table';
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { useQuery } from '@tanstack/react-query';
import './App.css';
// type Wine = { // 型定義は同じなので省略
// const columns: ColumnDef<Wine>[] = [ // カラム定義は同じなので省略
export default function App() {
const [pagination, setPagination] =
useState({pageIndex: 0, pageSize: 10}); // ← ①
const { data, isLoading } = useQuery({ // ← ②
queryKey: ['wines'],
queryFn: async () => {
const res = await fetch('http://localhost:3030/wines?_limit=200');
return res.json()
},
});
const table = useReactTable<Wine>({ // ← ③
columns,
data,
debugTable: true, // ← ④
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), // ← ⑤
onPaginationChange: setPagination, // ← ⑥
state: { pagination }, // ← ⑦
});
if (isLoading) {
return <p>Loading...</p>;
} else {
return (
<>
<h1>Wine</h1>
<div style={{margin: '0 0 10px 20px'}}>
<button onClick={() => table.previousPage()} {// ← ⑧ }
disabled={!table.getCanPreviousPage()}>
<
</button>
<span style={{margin: '0 10px'}}>
{pagination.pageIndex + 1} / {table.getPageCount()} {// ← ⑨ }
</span>
<button onClick={() => table.nextPage()} {// ← ⑩ }
disabled={!table.getCanNextPage()}>
>
</button>
</div>
<table>
・・・・ テーブル表示のコードは同じなので省略
</table>
</>
)
}
}
Manual Server-Side Pagination
前のページネーションでは最初に全データを取得してしまいますが、TanStack Tableにはバックエンドのページネーション機能を使い必要なページのみクライアント側で扱うページネーションも可能です。
コードの変更点は、今回も僅かです
- ① ページング用のステートpagination定義は同じ
- ② 全データ件数を保存するためのステートrowCountを定義
- ③ useQueryのキャッシュ機能を使うためにkeyにページ番号を追加しページ単位でキャッシュするように設定
- ④ json-serverのページネーションを使いページ単位でデタを取得
- _pageでページ番号、_per_pageでページの大きさを指定
- ⑤ json-serverの戻り値のヘッダー情報
X-Total-Count
に全データ件数が入っているので、これをrowCountテートに保存 - ⑥ キャッシュの廃棄時間を設定
- ⑦ テーブル管理用の
table
オブジェクトの作成 - ⑧ コードでページネーションする設定
- ⑨ 全データの件数を指定
import { useState } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import { flexRender, getCoreRowModel, useReactTable} from '@tanstack/react-table';
import './App.css'
import { useQuery } from '@tanstack/react-query';
// type Wine = { // 型定義は同じなので省略
// const columns: ColumnDef<Wine>[] = [ // カラム定義は同じなので省略
export default function App() {
const [pagination, setPagination] =
useState({pageIndex: 0, pageSize: 10}); // ← ①
const [rowCount, setRowCount] = useState(1); // ← ②
const { data, isLoading } = useQuery({
queryKey: ['wines', pagination.pageIndex], // ← ③
queryFn: async () => {
const res = await fetch(`http://localhost:3030/wines?` + // ← ④
`_page=${pagination.pageIndex + 1}&_per_page=${pagination.pageSize}`);
setRowCount(Number(res.headers.get("X-Total-Count") ?? "1")); // ← ⑤
return res.json();
},
staleTime: 5000 // ← ⑥
});
const table = useReactTable<Wine>({ // ← ⑦
columns,
data,
debugTable: true,
getCoreRowModel: getCoreRowModel(),
manualPagination: true, // ← ⑧
rowCount: rowCount, // ← ⑨
onPaginationChange: setPagination,
state: { pagination },
});
// 表示(JSX)はClient-Side Paginationと同じ
Material UIにしてみた
Manual Server-Side PaginationのコードをMaterial UIできれいな見かけにしてみました。
- ① 1行毎にバックグラウンドの色を変えたり最終行の線を消したテーブル行コンポーネント
- ② Material UIのテーブルのページネーションコンポーネント
<TablePagination>
を使ってみました- 必要な情報や関数はtableオブジェクトやpagnationオブジェクトから取得しています
import { useState } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import { flexRender, getCoreRowModel, useReactTable} from '@tanstack/react-table';
import { Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead,
TablePagination, TableRow, Typography, styled } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
// type Wine = { // 型定義は同じなので省略
// const columns: ColumnDef<Wine>[] = [ // カラム定義は同じなので省略
const StyledTableRow = styled(TableRow)(({ theme }) => ({ // <- ①
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.action.hover,
},
'&:last-child td, &:last-child th': {
border: 0,
},
}));
export default function App() {
// Appの最初の部分は同じなので省略
if (isLoading) {
return <p>loading...</p>;
} else {
return (
<Box sx={{p: 4}}>
<Typography variant="h2" gutterBottom>Wine</Typography>
<TableContainer component={Paper} sx={{p: 2}}>
<Table>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableCell key={header.id} colSpan={header.colSpan}
sx={{fontWeight: 600}}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{table.getRowModel().rows.map((row) => {
return (
<StyledTableRow key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
);
})}
</StyledTableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<TablePagination // <- ②
rowsPerPageOptions={[-1]}
component="div"
count={table.getPageCount()}
rowsPerPage={pagination.pageSize}
page={pagination.pageIndex}
onPageChange={(_e: unknown, newPage: number) =>
table.setPageIndex(newPage)}
/>
</Box>
)
}
}
まとめ
再度かきますがTanStack TableにはUI要素は含んでないライブラリーで、ソートやページング、フィルタリング、カラムの並び替えなどの高機能なテーブルを実現するためのデータ管理やAPIを提供してくれるライブラリーです。
UIが自由に選べるのは素晴らしいことだと思います。さらにReact以外VueやSolid, Svelte、さらに素のJavaScriptでも使えるようです。
他にもTanStackにはおもしろそうなライブラリーがあるので機会があれば取り上げたいと思います。