tanstack/react-tableの使い方
投稿日: 2024年12月03日
実務課題で指定されていて初めましてだったreact-tableですが、実務では結構使われていると聞きました。
最初は難しいという印象を受けたのですが、私は実務課題の後に個人開発の第二弾で復習も兼ねて使うことにして、その後頂いた案件では指定はなかったものの、react-tableを5-6ページで使いました。
慣れると難しくなくなります。
やっぱり何度も書く。違うパターンで書く。練習だけの為だと私はあまりやる気がでないので個人開発や実務で使うということの大切さはとても感じました。
あまりreact-tableの私にとって有益な記事がなかった記憶があるので、この記事では初めてreact-tableを使う方に向けて使い方をご紹介したいと思います。
私の個人開発第二弾のテーブルが非常にシンプルなので、その時のコードを解説する形でいきます。
完成したテーブルはこんな感じになります。
npm install @tanstack/react-table
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
すでに色々出てきて、早速これらは一体何??ってなりますよね。。。
一旦ここで1つずつ見てきます。
createColumnHelper
:名の通り、カラム(列データ)を作成するための機能を提供してくれるヘルパー関数です。めちゃくちゃ便利です。
flexRender
:テーブルのセルやヘッダーをレンダリングするために使用される関数です。これが定義したテーブルの情報を良い感じに呼び出すための機能を提供してくれます。
getCoreRowModel
:テーブルの基本的な行モデルを取得するための関数です。データのフィルタリング、ソート、グループ化、ページネーションなどの操作を行うための基礎を提供します。
useReactTable
:テーブルの状態とロジックを管理するためのフックです。
記事を書くにあたりgetCoreRowModelってホントにいるんかいな?と思って消してみたらエラー吐きました。ソートやフィルタリングを行わない場合でも、テーブルの行データをどのように取得し、構築するかを定義するために必要です。
"use client";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Task } from "@/app/_types/room/[id]/IndexResponse";
import { IndexResponse } from "@/app/_types/room/[id]/IndexResponse";
import { KeyedMutator } from "swr";
import { dayjs } from "@/app/_utils/dayjs";
import { TaskDetailModal } from "./TaskDetailModal";
import { useState } from "react";
const columnHelper = createColumnHelper<Task>();
const columns = [
columnHelper.accessor("date", {
header: "日付",
cell: info => dayjs.tz(info.getValue(), "Asia/Tokyo").format("YYYY/M/D"),
}),
columnHelper.accessor("task", {
header: "予定",
cell: info => info.getValue(),
}),
columnHelper.accessor("schedules", {
header: "通知設定",
cell: info => (
<div>
{info.getValue().map((item, index) => (
<div
key={index}
className=""
>{`${item.daysBefore}日前:${item.hour}時`}</div>
)
)}
</div>
),
}),
];
interface Props {
taskData: Task[];
mutate: KeyedMutator<IndexResponse>;
}
export const TaskIndex: React.FC<Props> = ({ taskData, mutate }) => {
const table = useReactTable({
data: taskData || [],
columns,
getCoreRowModel: getCoreRowModel(),
});
const [isOpen, setIsOpen] = useState(false);
const [selectedTaskId, setSelectedTaskId] = useState<number | null>(null);
return (
<div className="mx-auto pt-3 px-1 w-full">
<table className="table-fixed mb-10 w-full">
<thead className="bg-gray-200">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => {
const headerContent = flexRender(
header.column.columnDef.header,
header.getContext()
);
return (
<th
key={header.id}
className={`p-2 text-left font-normal text-xs ${
headerContent === "日付"
? "w-[100px]"
: headerContent === "通知設定"
? "w-[90px]"
: "w-auto"
}`}
>
{headerContent}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr
key={row.id}
className="border-b-[1px] border-gray_bg cursor-pointer"
onClick={() => {
setSelectedTaskId(row.original.taskId);
setIsOpen(true);
}}
>
{row.getVisibleCells().map(cell => {
const columnContent = flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
return (
<td key={cell.id} className={`p-1 font-normal text-xs`}>
{columnContent}
</td>
);
})}
</tr>
))}
</tbody>
</table>
{selectedTaskId && (
<TaskDetailModal
isOpen={isOpen}
closeModal={() => {
setIsOpen(false);
setSelectedTaskId(null);
}}
mutate={mutate}
taskId={selectedTaskId}
/>
)}
</div>
);
};
クリックしたら編集モーダル開いたりする機能を含むので少し長いですが、テーブルに関わるところだけ詳しく見ていきます。
下記の箇所で列データの定義を行っています。
const columnHelper = createColumnHelper<Task>();
const columns = [
columnHelper.accessor("date", {
header: "日付",
cell: info => dayjs.tz(info.getValue(), "Asia/Tokyo").format("YYYY/M/D"),
}),
columnHelper.accessor("task", {
header: "予定",
cell: info => info.getValue(),
}),
columnHelper.accessor("schedules", {
header: "通知設定",
cell: info => (
<div>
{info.getValue().map((item, index) => (
<div
key={index}
className=""
>{`${item.daysBefore}日前:${item.hour}時`}</div>
)
)}
</div>
),
}),
];
ここでTask
型に基づいてテーブル列を定義するためのヘルパーを作成します。
const columnHelper = createColumnHelper<Task>();
Task型は下記のように別ファイルで定義しています。
export interface Schedule {
daysBefore: number;
hour: number;
}
export interface Task {
taskId: number;
date: Date;
task: string;
schedules: Schedule[];
}
次に列を定義します。ここで前の行で定義したヘルパーを使ってそれぞれの列がどのようにデータを取得し、表示するかを指定していきます。
具体的な例を見る前に使い方をザっとご説明します。
列のデータをどのプロパティから取得するかを指定します。
型情報にあるプロパティが設定できます(つまり補完あり)。
columnHelper.accessor("hoge", {...})
ヘッダーに表示する文字を指定します。
ここは型の情報等関係なく、表示したいように設定することが出来ます。
header: "hoge",
各セルの表示内容を定義します。infoは引数で、色んなデータをここから取得することが出来ますが、そのまま表示したい時はinfo.getValue()
でOKです。
cell: info => {}
同じ行の他のデータを取得したい場合は、
info.row.original
から取得することも出来ます。
例えば、埋め込みたいリンクに他のデータ(例えばidとか)がある場合は、
info.row.original.id
として使ったりします。
今回の例では使用していませんが、結構頻出な気がします。
columnHelper.accessor("schedules", {
header: "通知設定",
cell: info => (
<div>
{info.getValue().map((item, index) => (
<div
key={index}
className=""
>{`${item.daysBefore}日前:${item.hour}時`}</div>
)
)}
</div>
),
}),
schedulesというプロティからデータを取得して
columnHelper.accessor("schedules", {
列ヘッダーには通知設定と表示
header: "通知設定",
セルには取得したデータをどう表示したいか定義できるので、すべて表示できるようにmap処理しています。
cell: info => (
<div>
{info.getValue().map((item, index) => (
<div
key={index}
className=""
>{`${item.daysBefore}日前:${item.hour}時`}</div>
)
)}
</div>
),
何日前の何時か表示したいのでそのようにしています。
他の案件でやりましたが、例えばここが備考入力用のテキストエリアだったり、ステータスの更新をおこなうセレクトボックスだったりすることもあります。
セルの中に表示したい内容はすべてここに書きます。
テーブルの設定を行います。
これにより指定されたデータと列に基づいてテーブルを構築することができます。
const table = useReactTable({
data: taskData || [],
columns,
getCoreRowModel: getCoreRowModel(),
});
一行ずつ見ていきます。
テーブルに表示するデータを指定(taskDateにはTask型の配列が入っています)
data: taskData || [],
テーブルの列の定義を指定(columnHelperで定義した内容)
columns,
テーブルの基本的な行モデルを取得するための関数を指定
getCoreRowModel: getCoreRowModel()
これでレンダリングの準備は整いました!!
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => {
const headerContent = flexRender(
header.column.columnDef.header,
header.getContext()
);
return (
<th
key={header.id}
className={`p-2 text-left font-normal text-xs ${
headerContent === "日付"
? "w-[100px]"
: headerContent === "通知設定"
? "w-[90px]"
: "w-auto"
}`}
>
{headerContent}
</th>
);
})}
</tr>
))}
ヘッダーのグループを取得します。これは、複数行のヘッダーがある場合に対応します。
table.getHeaderGroups()
各ヘッダーグループ内の個々のヘッダーオブジェクトの配列です。
各ヘッダーは、列の設定情報を持っています。
headerGroup.headers
ヘッダーの内容をレンダリングします。
flexRender
列定義のヘッダーコンテンツを取得します。
header.column.columnDef.header
レンダリングに必要なコンテキスト情報の取得します。
header.getContext()
これでヘッダー部分のレンダリングはOKです。
{table.getRowModel().rows.map(row => (
<tr
key={row.id}
className="border-b-[1px] border-gray_bg cursor-pointer"
onClick={() => {
setSelectedTaskId(row.original.taskId);
setIsOpen(true);
}}
>
{row.getVisibleCells().map(cell => {
const columnContent = flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
return (
<td key={cell.id} className={`p-1 font-normal text-xs`}>
{columnContent}
</td>
);
})}
</tr>
))}
現在の行モデルの行データを取得します。
table.getRowModel().rows
現在の行における表示可能なセルの配列を取得します。
各セルオブジェクトは、特定の列のデータを保持しています。
row.getVisibleCells()
セルの内容をレンダリングします。(ヘッダーでも出てきましたが)
flexRender
セルのレンダリングに必要なコンテンツを取得します。
cell.column.columnDef.cell
レンダリングに必要なコンテキスト情報を取得します。
cell.getContext()
この記述はセルの出力をするときの定番だと思います。
const columnContent = flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
モーダルとか関係ない記述ありますが、スルーさせていただきますw
やっぱりあまり簡単ではないと思いますが、テーブルの実装することがありましたら参考にしてください。
見ながら書けば簡単なテーブルは実装できると思います。
私はいつも自力でいちから書こうとする気は一ミリもなくて、コピペしてきて必要な箇所変えてます笑
難易度が変わってくるとしたらセルを定義する箇所の中身じゃないでしょうか・・
ガチ実務案件ではセルの中が色々で、コンポーネントが入り込むことも普通にあったりしました。
ちなみにセルの中で値を変更したりしてステート管理する必要がある場合は、ここでしようとせずに、別コンポーネントに切り出してそっち側で管理しないと大変なことになります。
私はまだ使ったことないですが、ソートとかフィルタリングの機能もあるらしいのでまた機会があれば書いてみたいです!