tanstack/react-tableの使い方

tanstack/react-tableの使い方

投稿日: 2024年12月03日

学習振り返り
Tips
要約
  • この記事では、react-tableの基本的な使い方と、個人プロジェクトでの実装例を紹介しています。
  • createColumnHelper、flexRender、getCoreRowModelなどの主要な機能を解説し、具体的なコード例を示します。
  • テーブルのセルやヘッダーのカスタマイズ方法や、モーダルを開く機能などを通じて、実務での活用方法を学ぶことができます。

はじめに

実務課題で指定されていて初めましてだったreact-tableですが、実務では結構使われていると聞きました。

最初は難しいという印象を受けたのですが、私は実務課題の後に個人開発の第二弾で復習も兼ねて使うことにして、その後頂いた案件では指定はなかったものの、react-tableを5-6ページで使いました。

慣れると難しくなくなります。

やっぱり何度も書く。違うパターンで書く。練習だけの為だと私はあまりやる気がでないので個人開発や実務で使うということの大切さはとても感じました。

あまりreact-tableの私にとって有益な記事がなかった記憶があるので、この記事では初めてreact-tableを使う方に向けて使い方をご紹介したいと思います。

私の個人開発第二弾のテーブルが非常にシンプルなので、その時のコードを解説する形でいきます。

仕上がり

tanstack/react-tableの使い方|ShiftBブログ

完成したテーブルはこんな感じになります。

インストール

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>
  );
};

クリックしたら編集モーダル開いたりする機能を含むので少し長いですが、テーブルに関わるところだけ詳しく見ていきます。

解説

createColumnHelper

下記の箇所で列データの定義を行っています。

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

として使ったりします。

今回の例では使用していませんが、結構頻出な気がします。

1つ具体的に見てみる

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>
    ),

何日前の何時か表示したいのでそのようにしています。

他の案件でやりましたが、例えばここが備考入力用のテキストエリアだったり、ステータスの更新をおこなうセレクトボックスだったりすることもあります。

セルの中に表示したい内容はすべてここに書きます。

useReactTable

テーブルの設定を行います。

これにより指定されたデータと列に基づいてテーブルを構築することができます。

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

おわりに

やっぱりあまり簡単ではないと思いますが、テーブルの実装することがありましたら参考にしてください。

見ながら書けば簡単なテーブルは実装できると思います。

私はいつも自力でいちから書こうとする気は一ミリもなくて、コピペしてきて必要な箇所変えてます笑

難易度が変わってくるとしたらセルを定義する箇所の中身じゃないでしょうか・・

ガチ実務案件ではセルの中が色々で、コンポーネントが入り込むことも普通にあったりしました。

ちなみにセルの中で値を変更したりしてステート管理する必要がある場合は、ここでしようとせずに、別コンポーネントに切り出してそっち側で管理しないと大変なことになります。

私はまだ使ったことないですが、ソートとかフィルタリングの機能もあるらしいのでまた機会があれば書いてみたいです!

シェア!

Threads
user
吉本茜
山口在住/二児の母/育休中
Loading...
記事一覧に戻る
Threads
0