1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】TanStack Table(React Table)をハンズオンで学ぼう!

Last updated at Posted at 2025-04-10

本稿の目的と対象読者

本稿の目的は、TanStack Table(React Table)の基本的な概念と主要な機能を理解し、読者が自身のReactプロジェクトで効率的かつ柔軟なテーブル実装ができるようになることです。

Reactを学び始めたばかりの方でも、TanStack Tableの基本的な使い方を無理なく理解できるよう、「何をやりたいならどうやる」という実践的なHow to形式で解説を進めます。

各機能の解説では、具体的なコード例を豊富に盛り込み、それぞれのコードがどのような役割を果たしているのか、なぜそのように記述するのかを丁寧に解説します。

Reactの基礎を理解している方であれば、実際に手を動かしながらTanStack Tableの使い方を習得できるはずです。

取り扱うHow to

0. はじめに

そもそもTanStack Tableとは?

TanStack Tableは、Reactに限らず、Vue、Solid、Svelte、Litといった多様なJavaScriptフレームワークで利用できる、ヘッドレスUIテーブルライブラリです。

ヘッドレスUIとは?

UIコンポーネントの見た目を提供せず、機能、ロジック、状態管理だけを提供するライブラリやコンポーネントのことを指します。

コンポーネントの「頭(Head)」、つまり見た目の部分を取り除き、機能やロジックという「体(Body)」だけを提供することで、開発者に最大限の柔軟性とコントロールを与えるアプローチです。

TanStack Tableは、React Table v7の後継として開発され、TypeScriptで完全に書き直されたライブラリです。
これにより、以前のバージョンに比べて型安全性が向上し、より堅牢なテーブル実装が可能になっています。

TanStack Tableの設計思想の中心にあるのは、機能性と柔軟性の両立です。
UIのデザインは開発者に完全に委ねる一方で、テーブルの操作に必要な複雑なロジックはライブラリが提供するため、効率的かつ高度なテーブル機能を実装できるのが大きな魅力です。

なぜTanStack Tableを使うのか?

主な利点としてまず挙げられるのはその豊富な機能です。

標準でソートフィルタリングページネーションなどの機能がサポートされており、これらの機能を組み合わせることで、複雑なデータ操作を必要とするテーブルも比較的容易に実装できます。

また、TanStack Tableは軽量でありながら、大規模なデータセットの処理にも非常に適しています。
これは、表示に必要なデータのみを効率的に処理する設計になっているためです。

さらに、TanStack Tableは高いカスタマイズ性を誇ります。
ほぼ全ての要素に対して、開発者のニーズに合わせて挙動や表示方法を変更できます。

そして、TanStack Tableは拡張性にも優れています。
基本的な機能に加えて、独自のロジックや機能を追加することも容易です。

つまり......、TanStack Tableは、Reactアプリケーションにおいて、パフォーマンスが求められる大規模データを取り扱う場合や、デザインの自由度を重視する場合に非常に有効な選択肢となると言えます。

機能 利点
ヘッドレスUI スタイリングとマークアップを完全に制御可能。あらゆるUIライブラリと統合可能
フレームワークに依存しない React、Vue、Solid、Svelteなど、様々なフレームワークで使用可能
コア機能(ソート、フィルタ、ページネーション) 一般的なテーブル操作に必要な機能が組み込み済み
軽量 バンドルサイズが小さく、ロード時間の短縮に貢献
大規模データセットに対応 大量のデータを効率的に表示・操作
高いカスタマイズ性と拡張性 特定のプロジェクト要件に合わせて調整可能。独自の機能を追加可能

1. 基本的なテーブルの表示

最も基本的なテーブルを表示する方法

ReactでTanStack Tableを使って最も基本的なテーブルを表示するには、いくつかのステップを踏む必要があります。

ここでは、「手元のデータを画面にテーブルとして表示したいんだけど、どうすればいいの?」 という疑問に答える形で、具体的な手順を解説します。

まずはインストール

# npm
npm install @tanstack/react-table

# yarn
yarn add @tanstack/react-table

# pnpm
pnpm add @tanstack/react-table

関連パッケージ(必要に応じて)

# 仮想化スクロール
npm install @tanstack/react-virtual

# クエリ
npm install @tanstack/react-query

データの準備

まず最初に、テーブルに表示したいデータを用意します。

TanStack Tableでは、データをJavaScriptのオブジェクトを要素とする配列として定義します。
配列の各オブジェクトがテーブルの1行に対応し、オブジェクトの各プロパティがテーブルの列に対応します。

JavaScriptの基本的なデータ構造に慣れている方であれば、この形式は直感的で理解しやすいでしょう。

例えば、以下のような社員の情報を表示したいとします。


const data = [
  // 社員データ
  { id: 1, firstName: '太郎', lastName: '山田', age: 30 },
  { id: 2, firstName: '花子', lastName: '佐藤', age: 25 },
  { id: 3, firstName: '健太', lastName: '田中', age: 35 },
];

列の定義

次に、テーブルのを定義します。
ここでは、各列のヘッダー(表示名) と、どのデータプロパティをその列に表示するか を指定します。

TanStack Tableでは、@tanstack/react-tableから提供されるcreateColumnHelperという関数を使うと、列の定義をより簡単に行うことができます。

以下のコードは、上記の社員データに対応する列を定義する例です。


import { createColumnHelper } from '@tanstack/react-table';

const columnHelper = createColumnHelper();

const columns = [
  columnHelper.accessor('id', { header: 'ID' }),
  columnHelper.accessor('firstName', { header: '' }),
  columnHelper.accessor('lastName', { header: '' }),
  columnHelper.accessor('age', { header: '年齢' }),
];

ここで、accessorKeyには、準備したデータオブジェクトのプロパティ名を指定します。
例えば、'id'を指定すると、各社員オブジェクトのidプロパティの値がその列に表示されます。
一方、headerには、テーブルのヘッダー部分に表示したい列の名前を指定します。

上記の例では、それぞれ'ID'、'名'、'姓'、'年齢'というヘッダーが表示されます。

useReactTableフックでインスタンス作成

データの準備と列の定義が完了したら、次に@tanstack/react-tableから提供されるuseReactTableというフックを使って、テーブルのインスタンスを作成します。
このフックに、先ほど準備したdatacolumnsを渡すことで、TanStack Tableが内部的なテーブルの状態を管理し始めます。

基本的なテーブル表示を行うためには、getCoreRowModel()という行モデルを取得するための関数も指定する必要があります。
これは、テーブルに渡された元のデータを基にした基本的な行モデルを提供するもので、TanStack Tableがどのように行データを管理するかを定義します。

以下は、useReactTableフックを利用してテーブルインスタンスを作成するコード例です。


import { useReactTable, getCoreRowModel } from '@tanstack/react-table';

const table = useReactTable({
  data: data,
  columns: columns,
  getCoreRowModel: getCoreRowModel(),
});

テーブルのレンダリング

最後に、useReactTableフックから取得したtableインスタンスの情報を使って、ReactのJSXで実際のHTMLテーブルをレンダリングします。

テーブルのヘッダー行のグループはtable.getHeaderGroups()で取得し、その中の各ヘッダーセルはheader.headersで取得できます。

同様に、テーブルの行データはtable.getRowModel().rowsで取得し、各行の表示可能なセルはrow.getVisibleCells()で取得します。

ここで重要なのがflexRenderという関数です。
これは、列の定義に基づいてヘッダーやセルの中身を簡単にレンダリングするためのヘルパー関数で、@tanstack/react-tableから提供されています。

以下は、上記のテーブルインスタンスを使ってHTMLテーブルをレンダリングするJSXのコード例です。


<table>
  <thead>
    {table.getHeaderGroups().map((headerGroup) => (
      <tr key={headerGroup.id}>
        {headerGroup.headers.map((header) => (
          <th key={header.id}>
            {header.isPlaceholder? null : flexRender(
              header.column.columnDef.header,
              header.getContext()
            )}
          </th>
        ))}
      </tr>
    ))}
  </thead>
  <tbody>
    {table.getRowModel().rows.map((row) => (
      <tr key={row.id}>
        {row.getVisibleCells().map((cell) => (
          <td key={cell.id}>
            {flexRender(
              cell.column.columnDef.cell,
              cell.getContext()
            )}
          </td>
        ))}
      </tr>
    ))}
  </tbody>
</table>

このコードでは、まずtable.getHeaderGroups()で取得したヘッダーグループをループ処理し、各グループ内のヘッダー情報をさらにループ処理して、<th>要素としてレンダリングしています。
同様に、table.getRowModel().rowsで取得した行データをループ処理し、各行内の表示可能なセル情報をループ処理して、<td>要素としてレンダリングしています。

flexRender関数は、それぞれのヘッダーやセルに対して、対応する列定義に基づいて適切な内容を表示する役割を果たします。

ソースコード

ここまでのソースコードを提示します。


"use client";

import React from "react";
import {
  createColumnHelper, // 列定義を簡単にするヘルパー
  flexRender, // ヘッダーやセルをレンダリングする関数
  getCoreRowModel, // 基本的な行モデルを取得する関数
  useReactTable, // テーブルインスタンスを作成するフック
  ColumnDef, // 列定義の型 (TypeScript用)
} from "@tanstack/react-table";

// データの型定義 (TypeScript)
type Person = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

// 1. データの準備
const data: Person[] = [
  { id: 1, firstName: "太郎", lastName: "山田", age: 30 },
  { id: 2, firstName: "花子", lastName: "佐藤", age: 25 },
  { id: 3, firstName: "健太", lastName: "田中", age: 35 },
];

// 2. 列の定義
const columnHelper = createColumnHelper<Person>();

// ColumnDef<Person>[] と型を明示的に指定します。
// 第一引数はデータの"行"の型、
// 第二引数は"列"の型です。
// TanStack Tableは列のアクセサーから適切な型を推論できるため、二番目の型パラメータを指定しなくても問題ありません。
const columns: ColumnDef<Person>[] = [
  // `accessor`の第一引数にデータキー、第二引数にオプション(ヘッダー名など)を指定
  columnHelper.accessor("id", {
    // header: 'ID',
    // cell オプションでセルのレンダリング方法をカスタマイズできますが、
    // 指定しない場合はデフォルトで値が表示されます。
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor("firstName", {
    header: "",
  }),
  columnHelper.accessor("lastName", {
    header: "",
  }),
  columnHelper.accessor("age", {
    header: "年齢",
  }),
];

const App = () => {
  // 3. useReactTable フックでテーブルインスタンスを作成
  const table = useReactTable({
    data, // テーブルに表示するデータ
    columns, // テーブルの列定義
    getCoreRowModel: getCoreRowModel(), // 行モデルを取得 (必須)
  });

  // 4. テーブルのレンダリング
  return (
    <div style={{ padding: "20px" }}>
      <h1>社員リスト</h1>
      <table>
        <thead>
          {/* ヘッダーグループを取得してループ */}
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {/* 各ヘッダーグループ内のヘッダーを取得してループ */}
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{ border: "1px solid black", padding: "8px" }}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        // ヘッダーの内容をレンダリング
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {/* 行モデルから行を取得してループ */}
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {/* 各行の表示可能なセルを取得してループ */}
              {row.getVisibleCells().map((cell) => (
                <td
                  key={cell.id}
                  style={{ border: "1px solid black", padding: "8px" }}
                >
                  {flexRender(
                    // セルの内容をレンダリング
                    cell.column.columnDef.cell,
                    cell.getContext()
                  )}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default App;

実行結果

image.png

2. テーブルの列を定義する

列の表示名やデータとの紐付けを設定する方法

前のセクションでは、基本的なテーブルを表示するために必要な列の定義について触れました。
ここでは列の定義方法 についてより詳しく解説します。

特に、「列の表示名はどうやって設定するの?」「データの中のどのプロパティをどの列に表示できるの?」 といった疑問に答えます。

基本的な列定義

テーブルの列を定義する際には、createColumnHelperを使って作成したcolumnHelperオブジェクトのメソッド (accessor, display, group) を利用します。

最も基本的な方法は、accessorメソッドを使用し、表示するデータのキー (accessorKey) とヘッダー (header) を指定することです。これは、前のセクションでも紹介した方法です。


const columns = [
  columnHelper.accessor('id', { header: 'ID' }),
  columnHelper.accessor('firstName', { header: '' }),
  columnHelper.accessor('lastName', { header: '' }),
  columnHelper.accessor('age', { header: '年齢' }),
];

ヘッダーの設定

headerオプションには、単純な文字列だけでなく、ReactコンポーネントやJSXも指定することができます。
これにより、ヘッダーにアイコンを表示したり、カスタムな要素を配置したり、ソート機能のUIを組み込んだりすることが可能になります。

例えば、'名'というヘッダーにアイコンを追加したい場合は、以下のように記述できます。


const columns = [
  columnHelper.accessor("firstName", {
    header: () => (
      <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
        <span role="img" aria-label="person">👤</span>
        <span></span>
      </div>
    ),
  }),
];

accessorKey

accessorKeyは、テーブルの各列がどのデータプロパティに対応するかをTanStack Tableに伝えるための設定です。
このキーに指定された文字列は、準備したデータ配列内の各オブジェクトのプロパティ名と一致している必要があります。

例えば、accessorKey: 'firstName'と設定した場合、TanStack Tableは各行のデータオブジェクトからfirstNameというプロパティの値を取り出し、その列に表示します。
もし、指定したaccessorKeyに対応するプロパティがデータオブジェクトに存在しない場合、その列には何も表示されないか、エラーが発生する可能性があります。

accessorKeyは、単にデータを表示するだけでなく、ソートやフィルタリングなどの機能においても、どのデータを操作の対象とするかを特定するために使用されます。

headerオプションの設定

headerオプションは、テーブルのヘッダー部分に表示するテキストやJSXを指定します。
基本的な使い方としては、列の表示名を表す文字列を設定します。


{ header: '年齢' }

しかし、前述のように、関数を指定することもでき、その場合は引数としてヘッダーに関する様々な情報が渡されます。
この関数内でJSXを返すことで、より複雑なヘッダーの表示を実現できます。

例えば、ソート機能と連携したヘッダーを作成する場合、ヘッダーがクリックされた際のソート処理や、現在のソート状態を示すアイコンの表示などを、この関数内で制御することが可能です。

ソースコード

ここまでのソースコードを提示します。


"use client";

import React from "react";
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
  ColumnDef,
  HeaderContext, // Headerに関数を使う場合、引数の型として利用
} from "@tanstack/react-table";

type Person = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

const data: Person[] = [
  { id: 1, firstName: "太郎", lastName: "山田", age: 30 },
  { id: 2, firstName: "花子", lastName: "佐藤", age: 25 },
  { id: 3, firstName: "健太", lastName: "田中", age: 35 },
];

const columnHelper = createColumnHelper<Person>();

// 列の定義
const columns: ColumnDef<Person>[] = [
  /* 【例1: 基本的な文字列指定】
      accessorの第一引数が `accessorKey` (データオブジェクトのキー)
      { header: '...' } でヘッダー表示名を文字列で指定 */
  columnHelper.accessor("id", {
    header: "🏷️ID",
  }),

  // 【例2: headerにJSXを使用する】
  columnHelper.accessor("firstName", {
    header: () => (
      <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
        <span role="img" aria-label="person">👤</span>
        <span></span>
      </div>
    ),
  }),

  // 【例3: header に関数を指定し、引数 context を利用】
  columnHelper.accessor("lastName", {
    // header オプションに関数を指定。引数 (ここでは context) が渡される
    header: (context: HeaderContext<Person>) => {
      // context オブジェクトには列やテーブルに関する情報が含まれる
      // console.log(context.column.id); = 'lastName'
      // 例えば「ソート状態などをcontextから取得してアイコン表示を切り替える」などが可能
      return (
        <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
          {/* 例: ここにソート用アイコンなどを追加できる */}
          <button onClick={() => context.column.toggleSorting()}>Sort</button></div>
      );
    },
  }),

  columnHelper.accessor("age", {
    header: "👶年齢",
    size: 80, // 列幅の指定なども可能
    // TanStack Tableでは、列幅を明示的に指定しない場合、デフォルトで150pxが適用される
  }),
];

const App = () => {
  const table = useReactTable({
    data,
    columns, // 上で定義した列設定を使用
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div style={{ padding: "20px" }}>
      <h1>社員リスト (列定義の詳細)</h1>
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{
                    border: "1px solid black",
                    padding: "8px",
                    textAlign: "left",
                    width: header.column.getSize(), // 列の幅を適用
                  }}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td
                  key={cell.id}
                  style={{
                    border: "1px solid black",
                    padding: "8px",
                    width: cell.column.getSize(), // 列の幅を適用
                  }}
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default App;

実行結果

image.png

3. データの並び替え

テーブルの列でデータを並び替えられるようにする方法

前セクションの最後で触れたように、テーブルに表示されたデータを、特定の列の値に基づいて昇順または降順に並び替えられるようにする機能を解説します。

ここでは、「特定の列をクリックしたら、その列の値で並び替えたいんだけど、どうすればいいの?」 という要望に応える方法を解説します。

ソート機能の追加

テーブルでソートを可能にするには、useReactTableフックにgetSortedRowModel()を追加するだけで、内部的にテーブルがソート可能な状態になります。

以前の基本的なテーブル表示のコードに、このgetSortedRowModel()を追加した例を以下に示します。


import { useReactTable, getCoreRowModel, getSortedRowModel } from '@tanstack/react-table';

const table = useReactTable({
  data: data,
  columns: columns,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(), // ソート機能を追加
});

デフォルトでは、クリックするたびに昇順、降順、ソートなしの状態が切り替わります。

getSortedRowModel

getSortedRowModel()は、ソートが適用された後の行データを提供します。
したがって、テーブルをレンダリングする際には、table.getRowModel().rowsの代わりに、このgetSortedRowModel()から得られる行データを利用する必要があります。

具体的には、レンダリング部分のコードは以下のようになります。


<tbody>
  {table.getSortedRowModel().rows.map((row) => (
    <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
        <td key={cell.id}>
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ))}
</tbody>

ソートの設定オプション

TanStack Tableでは、ソートの挙動をより細かく制御するための様々な設定オプションが用意されています。
これらのオプションは、各列の定義 (columnHelper.accessorの第2引数) で設定できます。

  • sortingFn
    • 特定の列に対してデフォルトのソート関数とは異なるカスタムのソートロジックを指定できます。
      • 例えば、文字列として保存されている数値を数値としてソートしたい場合や、独自の比較アルゴリズムを適用したい場合に便利です。
    • デフォルトでは、文字列、数値、Date型に対して昇順・降順のソートが可能です。
  • sortDescFirst
    • trueに設定すると、その列で最初にソートを行う際に降順から開始するように変更できます。
  • enableSorting
    • falseに設定すると、その列でのソート機能を無効にすることができます。
    • 特定の列はソートさせたくない場合に利用します。
  • enableMultiSort
    • falseに設定すると、その列でのマルチソート(通常はShiftキーを押しながらヘッダーをクリックすることで複数の列を同時にソートする機能)を無効にできます。
  • invertSorting
    • trueに設定すると、その列のソート順を反転させることができます。
      • 例えば、ランキングのような低い数値が良いとされるデータを昇順で表示する場合などに便利です。
  • sortUndefined
    • undefinedの値を持つデータをソートする際の扱いを指定できます ('first', 'last', false, -1, 1)

テーブル全体に対するソート も可能です。
useReactTableのオプションでenableMultiSortfalseに設定すると、テーブル全体のマルチソートを無効にしたり、maxMultiSortColCountで同時にソートできる列の数を制限したりできます。

また、現在のソート状態(どの列がどの方向にソートされているか)は、table.getState().sortingを通じて取得できます。

デフォルトでは、テーブルのヘッダーをクリックすることでソートが切り替わります。
Shiftキーを押しながらクリックすることで、複数の列でのソート(マルチソート)が可能です。
この挙動は、useReactTableのオプションにあるisMultiSortEventでカスタマイズすることもできます。

ソースコード

ここまでのソースコードを提示します。


'use client';

import React, { useState } from 'react'; // ソート状態管理のために useState をインポート
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
  ColumnDef,
  getSortedRowModel, // ソート機能に必要なモデルをインポート
  SortingState,     // ソート状態の型定義をインポート
} from '@tanstack/react-table';

type Person = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
}

const data: Person[] = [
  { id: 1, firstName: '太郎', lastName: '山田', age: 30 },
  { id: 2, firstName: '花子', lastName: '佐藤', age: 25 },
  { id: 3, firstName: '健太', lastName: '田中', age: 35 },
];

const columnHelper = createColumnHelper<Person>();

const columns: ColumnDef<Person>[] = [
  columnHelper.accessor('id', {
    header: 'ID',
    enableSorting: false, // この列ではソートを無効にする
  }),
  columnHelper.accessor('firstName', {
    header: "",
    sortDescFirst: true, // 最初に降順でソートする設定
  }),
  columnHelper.accessor('lastName', {
    header: "",
  }),
  columnHelper.accessor('age', {
    header: '年齢',
  }),
];

const App = () => {
  // === ソート状態管理 ===
  // SortingState は [{ id: '列ID', desc: true/false }, ...] という形式の配列
  const [sorting, setSorting] = useState<SortingState>([]);

  const table = useReactTable({
    data,
    columns,
    // === ソート関連の設定 ===
    state: { // テーブルの状態として React の state を渡す
      sorting,
    },
    onSortingChange: setSorting, // テーブル内部でソートが変更された時にstateを更新する関数を渡す
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(), // ソートモデルを有効にする
    // enableMultiSort: false, // 例: テーブル全体のマルチソートを無効にする場合
    // debugTable: true, // デバッグ用にテーブル情報をコンソールに出力
  });

  return (
    <div style={{ padding: '20px' }}>
      <h1>社員リスト (ソート機能付き)</h1>
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{
                    border: '1px solid black',
                    padding: '8px',
                    textAlign: 'left',
                    // ソート可能な列はクリックできることを示すカーソルにする
                    cursor: header.column.getCanSort() ? 'pointer' : 'default',
                    userSelect: 'none', // テキスト選択を防ぐ
                  }}
                  // ヘッダークリック時にソートをトグルする
                  onClick={header.column.getToggleSortingHandler()}
                >
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                    {header.column.getCanSort() && ( // ソート可能な列のみインジケーターを表示
                      <span>
                        {(() => {
                          const sortDir = header.column.getIsSorted();
                          if (sortDir === 'asc') return '';
                          if (sortDir === 'desc') return '';
                          return <span style={{ color: 'lightgray'}}> ▲▼</span>;
                        })()}
                      </span>
                    )}
                  </div>
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id} style={{ border: '1px solid black', padding: '8px' }}>
                  {flexRender(
                    cell.column.columnDef.cell,
                    cell.getContext()
                  )}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      {/* デバッグ用に現在のソート状態を表示 */}
      <pre>Current Sorting: {JSON.stringify(sorting, null, 2)}</pre>
    </div>
  );
}

export default App;

実行結果

image.png

4. データのフィルタリング

テーブルのデータを特定の条件で絞り込めるようにする方法

表示するデータが多くなってきた場合...、ユーザーは特定の条件に合致するデータだけを見たいと思うでしょう。

TanStack Tableは、このようなフィルタリング機能も簡単に実装できます。

ここでは、「特定のキーワードを入力したら、そのキーワードを含むデータだけを表示したいんだけど、どうすればいいの?」 という要望に応える方法を解説します。

フィルタリング機能の実装

テーブルでフィルタリングを可能にするには、useReactTableフックにgetFilteredRowModel()を追加します。

ソート機能と同様に、以前のコードにこのgetFilteredRowModel()を追加するだけで、TanStack Tableはフィルタリングの準備を整えます。


import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel } from '@tanstack/react-table';

const table = useReactTable({
  data: data,
  columns: columns,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(), // フィルタリング機能を追加
});

getFilteredRowModel

getFilteredRowModel()は、フィルタリングが適用された後の行データを提供します。したがって、テーブルをレンダリングする際には、table.getRowModel().rowstable.getSortedRowModel().rowsと同様に、このgetFilteredRowModel()から得られる行データを利用する必要があります。

レンダリング部分のコードは以下のようになります。


<tbody>
  {table.getFilteredRowModel().rows.map((row) => (
    <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
        <td key={cell.id}>
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ))}
</tbody>

フィルタリングのカスタマイズ方法

TanStack Tableでは、様々な方法でフィルタリングをカスタマイズできます。

  • 列ごとのフィルタリング
    • 各列に対して個別のフィルタリングUIを提供できます。

    • これは、特定の列の値に基づいて絞り込みを行いたい場合に便利です。

      • 各列のフィルター値を取得するにはcolumn.getFilterValue()を使用し、フィルター値を設定するにはcolumn.setFilterValue()を使用します。
      • これらの関数を、例えばテキスト入力フィールドのonChangeハンドラーに接続することで、ユーザーが入力した値に応じてリアルタイムにフィルタリングを行うことができます。
    
    {table.getHeaderGroups().map((headerGroup) => (
      <tr key={headerGroup.id}>
        {headerGroup.headers.map((header) => (
          <th key={header.id}>
            {header.isPlaceholder? null : flexRender(
              header.column.columnDef.header,
              header.getContext()
            )}
            {header.column.getCanFilter()? (
              <input
                type="text"
                value={header.column.getFilterValue()?? ''}
                onChange={(e) =>
                  header.column.setFilterValue(e.target.value)
                }
                placeholder="Filter"
              />
            ) : null}
          </th>
        ))}
      </tr>
    ))}
  • filterFnオプション
    • 列定義でfilterFnオプションを指定することで、カスタムのフィルタリング関数を適用できます。
    • これにより、数値の範囲指定や、複数の選択肢からの絞り込みなど、より高度なフィルタリング要件に対応できます。
    
    const numberRangeFilter = (row, columnId, filterValue) => {
      const rowValue = row.getValue(columnId);
      const [min, max] = filterValue;
      return rowValue >= min && rowValue <= max;
    };
    
    const columns =;
    
    const table = useReactTable({
      //...,
      filterFns: {
        numberRange: numberRangeFilter,
      },
    });
  • グローバルフィルタリング
    • 複数の列を対象とした検索機能も簡単に実装できます。
    • これにより、ユーザーは複数の列にまたがるキーワードでデータを検索できます。
    • 現在のグローバルフィルターの値はtable.getState().globalFilterで取得でき、table.setGlobalFilter()で値を設定します。
    <input
      type="text"
      value={table.getState().globalFilter?? ''}
      onChange={(e) => table.setGlobalFilter(e.target.value)}
      placeholder="テーブル内を検索"
    />

現在の列ごとのフィルターの状態はtable.getState().columnFiltersで、グローバルフィルターの状態はtable.getState().globalFilterでそれぞれ取得できます。

ソースコード

ここまでのソースコードを提示します。


"use client";

import React, { useState } from "react";
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
  ColumnDef,
  getSortedRowModel,
  SortingState,
  getFilteredRowModel, // フィルター機能に必要なモデルをインポート
  ColumnFiltersState, // 列フィルター状態の型定義をインポート
} from "@tanstack/react-table";

type Person = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

const data: Person[] = [
  { id: 1, firstName: "太郎", lastName: "山田", age: 30 },
  { id: 2, firstName: "花子", lastName: "佐藤", age: 25 },
  { id: 3, firstName: "健太", lastName: "田中", age: 35 },
  { id: 4, firstName: "一郎", lastName: "鈴木", age: 30 },
  { id: 5, firstName: "二郎", lastName: "佐藤", age: 28 },
];

const columnHelper = createColumnHelper<Person>();

const columns: ColumnDef<Person>[] = [
  columnHelper.accessor("id", {
    header: "ID",
    enableSorting: false,
    enableColumnFilter: false, // この列では列フィルターを無効にする
  }),
  columnHelper.accessor("firstName", {
    header: "",
    filterFn: "equalsString", // 完全一致のみ
  }),
  columnHelper.accessor("lastName", {
    header: "",
  }),
  columnHelper.accessor("age", {
    header: "年齢",
  }),
];

const App = () => {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); // 列フィルター状態
  const [globalFilter, setGlobalFilter] = useState<string>(""); // グローバルフィルター状態

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      columnFilters,
      globalFilter,
    },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters, // 列フィルターが変更されたらstateを更新
    onGlobalFilterChange: setGlobalFilter, // グローバルフィルターが変更されたらstateを更新
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(), // フィルターモデルを有効にする
    // debugTable: true,
  });

  return (
    <div style={{ padding: "20px" }}>
      <h1>社員リスト (ソート・フィルター機能付き)</h1>

      <div style={{ marginBottom: "15px" }}>
        <input
          type="text"
          value={globalFilter ?? ""}
          onChange={(e) => setGlobalFilter(e.target.value)}
          placeholder="テーブル全体を検索..."
          style={{ padding: "6px", fontSize: "1rem", width: "300px" }}
        />
      </div>

      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{
                    border: "1px solid black",
                    padding: "8px 12px",
                    textAlign: "left",
                    cursor: header.column.getCanSort() ? "pointer" : "default",
                    userSelect: "none",
                  }}
                  onClick={
                    header.column.getCanSort()
                      ? header.column.getToggleSortingHandler()
                      : undefined
                  }
                >
                  <div
                    style={{
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "space-between",
                      marginBottom: header.column.getCanFilter() ? "4px" : "0",
                    }}
                  >
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}

                    {header.column.getCanSort() && (
                      <span>
                        {(() => {
                          const sortDir = header.column.getIsSorted();
                          if (sortDir === "asc") return "";
                          if (sortDir === "desc") return "";
                          return (
                            <span style={{ color: "lightgray" }}> ▲▼</span>
                          );
                        })()}
                      </span>
                    )}
                  </div>

                  {header.column.getCanFilter() ? (
                    <div>
                      <input
                        type="text"
                        value={String(header.column.getFilterValue() ?? "")}
                        onChange={(e) =>
                          header.column.setFilterValue(e.target.value)
                        }
                        placeholder={`"${flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}" を検索`}
                        style={{
                          width: "100%",
                          padding: "4px",
                          boxSizing: "border-box",
                        }}
                        onClick={(e) => e.stopPropagation()} // ヘッダーのソートイベント発火を防ぐ
                      />
                    </div>
                  ) : null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td
                  key={cell.id}
                  style={{ border: "1px solid black", padding: "8px 12px" }}
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      {/* デバッグ用に現在のフィルター状態を表示 */}
      <pre>Column Filters: {JSON.stringify(columnFilters, null, 2)}</pre>
      <pre>Global Filter: {JSON.stringify(globalFilter, null, 2)}</pre>
    </div>
  );
};

export default App;

実行結果

image.png

5. ページネーション

大量のデータを扱う場合にテーブルを分割して表示する方法

先程に引き続き、表示するデータが非常に多い場合...、一度に全てのデータを表示するとパフォーマンスが悪くなったり、ユーザーが情報を探しにくくなったりすることがあります。

このような場合に有効なのがページネーションです。

TanStack Tableでは、データを複数のページに分割して表示する機能も簡単に実装できます。

ここでは、「データが多すぎて一度に表示しきれないから、ページを分けて表示したいんだけど、どうすればいいの?」 という要望に応える方法を解説します。

ページネーションの追加

大量のデータを扱う際に、テーブルをページ分割して表示するには、useReactTableフックにgetPaginationRowModel()を追加します。

ソート機能やフィルタリング機能と同様に、useReactTableのオプションにこの関数を追加するだけで、TanStack Tableはページネーションの準備を行います。


import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel } from '@tanstack/react-table';

const table = useReactTable({
  data: data,
  columns: columns,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(), // ページネーション機能を追加
});

getPaginationRowModel

getPaginationRowModel()は、現在のページに表示するべき行データを提供します。
したがって、テーブルをレンダリングする際には、table.getRowModel().rowsの代わりに、このgetPaginationRowModel()から得られる行データを利用する必要があります。

レンダリング部分のコードは以下のようになります。


<tbody>
  {table.getPaginationRowModel().rows.map((row) => (
    <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
        <td key={cell.id}>
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ))}
</tbody>

表示件数の制御方法

TanStack Tableでは、1ページあたりに表示するデータ件数を制御したり、現在のページ番号を変更したりするためのAPIが用意されています。

  • 現在の1ページあたりの表示件数は、table.getState().pagination.pageSizeで取得でき、table.setPageSize(size)で変更できます。通常、ユーザーが選択できるようなUIを提供します。
  • 現在のページ番号(0から始まる)は、table.getState().pagination.pageIndexで取得でき、特定のページに移動するにはtable.setPageIndex(index)を使用します 4。
  • 前のページへ移動するにはtable.previousPage()を、次のページへ移動するにはtable.nextPage()を呼び出します。
  • 前のページが存在するかどうかはtable.getCanPreviousPage()で、次のページが存在するかどうかはtable.getCanNextPage()で確認できます。これらの関数は、ページ移動ボタンの有効・無効を切り替える際に便利です。
  • 総ページ数はtable.getPageCount()で取得できます。

以下は、これらのAPIを利用した基本的なページネーションUIのコード例です。


<div>
  <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
    前へ
  </button>
  <span>
    {table.getState().pagination.pageIndex + 1} / {table.getPageCount()}
  </span>
  <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
    次へ
  </button>
  <select
    value={table.getState().pagination.pageSize}
    onChange={(e) => table.setPageSize(Number(e.target.value))}
  >
    {.map((pageSize) => (
      <option key={pageSize} value={pageSize}>
        {pageSize} 件表示
      </option>
    ))}
  </select>
</div>

このUIでは、前のページと次のページへの移動ボタン、現在のページ番号と総ページ数の表示、そして1ページあたりの表示件数を選択するドロップダウンリストが実装されています。

なお、上記はクライアントサイドでのページネーションの例ですが、データ量が非常に多い場合は、サーバーサイドでページネーションを行い、必要なデータのみをAPIから取得する方式も検討する必要があります。
TanStack Tableは、サーバーサイドのページネーションにも対応するための機能を提供しており、useReactTableのオプションなどを適切に設定することで実現できます。

PaginationState

PaginationStateをインポートして使用すると、ページネーションの状態を明示的に管理できるようになります。
これにより、例えばURLからページ番号を取得して初期表示したり、ページ変更時に特定の処理をトリガーしたりすることが可能になります。

  • ページネーション状態を外部から制御できる
  • URL パラメータと同期させるなどの応用が可能
  • ページネーション状態変更時に特定の処理(例:APIリクエストなど)を実行できる

以下のように実装します。


const [pagination, setPagination] = useState<PaginationState>({
  pageIndex: 0, // 初期ページ (0から始まる)
  pageSize: 10, // 1ページの表示件数
});

const table = useReactTable({
  data,
  columns,
  state: {
    sorting,
    pagination, // ページネーション状態を渡す
  },
  onSortingChange: setSorting,
  onPaginationChange: setPagination, // 状態変更ハンドラを渡す
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
});
  • 応用例(ページ変更時にスクロール位置をリセットする)

// pagination状態が変わったときに何か処理をする
useEffect(() => {
  // 例:ページトップにスクロール
  window.scrollTo(0, 0);
  
  // 例:新しいデータをフェッチ(サーバーサイドページネーション)
  // fetchData(pagination.pageIndex, pagination.pageSize);
}, [pagination]);

ソースコード

ここまでのソースコードを提示します。


"use client";

import React, { useState, useMemo } from "react";
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  getPaginationRowModel, // ページネーションモデルをインポート
  ColumnDef,
  flexRender,
  SortingState,
  PaginationState, // 【応用】必要に応じてインポート
} from "@tanstack/react-table";

type Person = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
  visits: number;
  status: string;
  progress: number;
};

// 日本人の姓リスト
const japaneseLastNames = [
  "佐藤",
  "鈴木",
  "高橋",
  "田中",
  "伊藤",
  "渡辺",
  "山本",
  "中村",
  "小林",
  "加藤",
  "吉田",
  "山田",
  "佐々木",
  "山口",
  "松本",
  "井上",
  "木村",
  "",
  "斎藤",
  "清水",
  "村上",
  "近藤",
  "坂本",
  "遠藤",
  "青木",
  "藤田",
  "岡田",
  "後藤",
  "石井",
  "橋本",
];

// 日本人の名リスト
const japaneseFirstNames = [
  "大翔",
  "",
  "陽翔",
  "悠真",
  "陽太",
  "陽菜",
  "",
  "",
  "結菜",
  "咲良",
  "颯真",
  "",
  "陽斗",
  "",
  "結衣",
  "さくら",
  "美咲",
  "",
  "",
  "優奈",
  "健太",
  "翔太",
  "拓海",
  "大輝",
  "康介",
  "美咲",
  "結衣",
  "花音",
  "綾乃",
  "真央",
];

// サンプルデータ
const makeData = (count: number): Person[] => {
  const data: Person[] = [];
  for (let i = 1; i <= count; i++) {
    // ランダムに姓と名を選択
    const lastName =
      japaneseLastNames[Math.floor(Math.random() * japaneseLastNames.length)];
    const firstName =
      japaneseFirstNames[Math.floor(Math.random() * japaneseFirstNames.length)];

    data.push({
      id: i,
      firstName: firstName,
      lastName: lastName,
      age: Math.floor(Math.random() * 60) + 20, // 20〜79歳
      visits: Math.floor(Math.random() * 1000),
      status: ["正社員", "契約社員", "パートタイム", "アルバイト"][
        Math.floor(Math.random() * 4)
      ],
      progress: Math.floor(Math.random() * 100),
    });
  }
  return data;
};

const App = () => {
  // サンプルデータを作成
  // useMemoでデータが再生成されるのを防ぐ
  const data = useMemo(() => makeData(100), []);

  // useMemoでカラム定義が再生成されるのを防ぐ
  const columns = useMemo<ColumnDef<Person>[]>(
    () => [
      {
        accessorKey: "id",
        header: "ID",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "firstName",
        header: "",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "lastName",
        header: "",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "age",
        header: "年齢",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "visits",
        header: "出社回数",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "status",
        header: "雇用形態",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "progress",
        header: "進捗",
        cell: (info) => info.getValue(),
      },
    ],
    []
  );

  // テーブルの状態管理 (ソート、ページネーションなど)
  const [sorting, setSorting] = useState<SortingState>([]);
  // 【応用】
  // ページネーションの状態もuseStateで管理する場合
  // const [pagination, setPagination] = useState<PaginationState>({
  //   pageIndex: 0, // 初期ページ (0から始まる)
  //   pageSize: 10, // 1ページの表示件数
  // });

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      // pagination, // 【応用】useStateで管理する場合
    },
    onSortingChange: setSorting,
    // onPaginationChange: setPagination, // 【応用】useStateで管理する場合
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(), // ページネーション機能を追加
    // debugTable: true,
  });

  return (
    <div style={{ padding: "20px" }}>
      <h1>社員リスト(ページネーション機能付き)</h1>
      <table style={{ borderCollapse: "collapse", width: "100%" }}>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{
                    border: "1px solid black",
                    padding: "8px",
                    cursor: "pointer",
                  }}
                  onClick={header.column.getToggleSortingHandler()}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                  {header.column.getIsSorted() === "asc"
                    ? "🔼"
                    : header.column.getIsSorted() === "desc"
                    ? "🔽"
                    : null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {/* ページネーションモデルから行を取得して表示 */}
          {table.getPaginationRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => (
                <td
                  key={cell.id}
                  style={{ border: "1px solid black", padding: "8px" }}
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>

      {/* ページネーションUI */}
      <div
        style={{
          marginTop: "20px",
          display: "flex",
          alignItems: "center",
          gap: "10px",
        }}
      >
        <button
          onClick={() => table.setPageIndex(0)} // 最初のページへ
          disabled={!table.getCanPreviousPage()}
        >
          {"<<"}
        </button>
        <button
          onClick={() => table.previousPage()} // 前のページへ
          disabled={!table.getCanPreviousPage()}
        >
          {"<"} 前へ
        </button>
        <span>
          Page{" "}
          <strong>
            {table.getState().pagination.pageIndex + 1} of{" "}
            {table.getPageCount()}
          </strong>
        </span>
        <button
          onClick={() => table.nextPage()} // 次のページへ
          disabled={!table.getCanNextPage()}
        >
          次へ {">"}
        </button>
        <button
          onClick={() => table.setPageIndex(table.getPageCount() - 1)} // 最後のページへ
          disabled={!table.getCanNextPage()}
        >
          {">>"}
        </button>
        <span style={{ marginLeft: "auto" }}>|</span>
        <span>
          Go to page:
          <input
            type="number"
            defaultValue={table.getState().pagination.pageIndex + 1}
            onChange={(e) => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0;
              table.setPageIndex(page);
            }}
            style={{ width: "50px", marginLeft: "5px" }}
          />
        </span>
        <select
          value={table.getState().pagination.pageSize}
          onChange={(e) => {
            table.setPageSize(Number(e.target.value));
          }}
        >
          {[10, 20, 30, 40, 50].map(
            (
              pageSize // 表示件数の選択肢
            ) => (
              <option key={pageSize} value={pageSize}>
                Show {pageSize}
              </option>
            )
          )}
        </select>
      </div>
      {/* デバッグ情報 */}
      <pre>{JSON.stringify(table.getState(), null, 2)}</pre>
    </div>
  );
};

export default App;

実行結果

image.png

6. 行の選択

テーブルの行を選択できるようにする方法

テーブルに表示された特定の行に対して操作(削除、編集、詳細情報の表示など)を行いたい場合、ユーザーが行を選択できる機能が必要になります。

TanStack Tableでは、行の選択機能も比較的簡単に実装できます。

ここでは、「テーブルの行をクリックしたり、チェックボックスを使ったりして選択できるようにしたいんだけど、どうすればいいの?」 という要望に応える方法を解説します。

行選択機能の実装

テーブルの行を選択できるようにするには、useReactTableフックのオプションでenableRowSelection: trueを設定し、getRowModel: getRowSelectionRowModel()を追加します。
また、選択状態を管理するためにuseStateを使用し、その状態と更新関数をuseReactTableに渡します。


import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getRowSelectionRowModel } from '@tanstack/react-table';
import { useState } from 'react';

const = useState({});

const table = useReactTable({
  data: data,
  columns: columns,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  getRowSelectionRowModel: getRowSelectionRowModel(), // 行選択機能を追加
  state: {
    rowSelection: rowSelection,
  },
  onRowSelectionChange: setRowSelection,
  enableRowSelection: true, // 行選択を有効にする
});

チェックボックスの追加

各行にチェックボックスを追加することで、ユーザーは複数の行を同時に選択できるようになります。
また、ヘッダー行にチェックボックスを追加することで、全ての行を一度に選択・解除できる機能も実装できます。

以下は、各行にチェックボックスを追加するJSXのコード例です。


<tbody>
  {table.getPaginationRowModel().rows.map((row) => (
    <tr key={row.id}>
      <td>
        <input
          type="checkbox"
          checked={row.getIsSelected()}
          onChange={row.getToggleSelectedHandler()}
        />
      </td>
      {row.getVisibleCells().map((cell) => (
        <td key={cell.id}>
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ))}
</tbody>

このコードでは、各行の先頭のセルに<input type="checkbox" />を追加しています。
row.getIsSelected()は、その行が選択されているかどうかを示す真偽値を返し、row.getToggleSelectedHandler()は、チェックボックスの状態が変更された際にその行の選択状態を切り替えるためのハンドラー関数を返します。

また、ヘッダー行に全ての行を選択・解除するためのチェックボックスを追加する例は以下の通りです。


<thead>
  <tr>
    <th>
      <input
        type="checkbox"
        checked={table.getIsAllRowsSelected()}
        onChange={table.getToggleAllRowsSelectedHandler()}
      />
    </th>
    {table.getHeaderGroups()?.headers.map((header) => (
      <th key={header.id}>
        {header.isPlaceholder? null : flexRender(
          header.column.columnDef.header,
          header.getContext()
        )}
      </th>
    ))}
  </tr>
</thead>

table.getIsAllRowsSelected()は全ての行が選択されているかどうかを返し、table.getToggleAllRowsSelectedHandler()は全ての行の選択状態を切り替えるためのハンドラー関数を返します。

選択されたデータの取得方法

選択された行のデータを取得するには、table.getSelectedRowModel().rowsを使用します。
これは、選択されている行のRowオブジェクトを含む配列を返します。各Rowオブジェクトのoriginalプロパティには、元のデータオブジェクトが格納されています。


const selectedRows = table.getSelectedRowModel().rows.map(row => row.original);
// console.log(selectedRows); // 選択されたデータの配列

ソースコード

ここまでのソースコードを提示します。


"use client";

import React, { useState } from "react";
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  flexRender,
  ColumnDef, // カラム定義の型
  RowSelectionState, // 行選択状態の型
} from "@tanstack/react-table";

type Person = {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
};

const defaultData: Person[] = [
  { id: 1, firstName: "太郎", lastName: "山田", age: 28 },
  { id: 2, firstName: "花子", lastName: "佐藤", age: 25 },
  { id: 3, firstName: "裕太", lastName: "鈴木", age: 30 },
  { id: 4, firstName: "春子", lastName: "蒲田", age: 22 },
  { id: 5, firstName: "一郎", lastName: "", age: 35 },
];

const App = () => {
  const [data, setData] = useState<Person[]>(() => [...defaultData]);
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); // 行選択状態を管理

  const columns = React.useMemo<ColumnDef<Person>[]>(
    () => [
      // 1. 行選択用のチェックボックスカラム
      {
        id: "select",
        header: ({ table }) => (
          <input
            type="checkbox"
            checked={table.getIsAllRowsSelected()}
            onChange={table.getToggleAllRowsSelectedHandler()} // 全選択/解除
          />
        ),
        cell: ({ row }) => (
          <input
            type="checkbox"
            checked={row.getIsSelected()}
            onChange={row.getToggleSelectedHandler()} // 個別行選択
          />
        ),
      },
      // 2. データ表示用のカラム
      {
        accessorKey: "firstName",
        header: "",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "lastName",
        header: "",
        cell: (info) => info.getValue(),
      },
      {
        accessorKey: "age",
        header: "年齢",
        cell: (info) => info.getValue(),
      },
    ],
    []
  );

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onRowSelectionChange: setRowSelection, // 行選択状態の更新関数を渡す
    state: {
      rowSelection: rowSelection, // 行選択状態を渡す
    },
    enableRowSelection: true, // 行選択を有効にする
  });

  // 選択された行のデータを取得
  const selectedRowsData = table
    .getSelectedRowModel()
    .rows.map((row) => row.original);

  return (
    <div style={{ padding: "20px" }}>
      <h1>社員リスト(行選択機能付き)</h1>
      <table
        style={{
          width: "100%",
          borderCollapse: "collapse",
          marginTop: "10px",
          border: "1px solid #ddd",
        }}
      >
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  colSpan={header.colSpan}
                  style={{
                    padding: "8px",
                    border: "1px solid #ddd",
                    backgroundColor: "#f2f2f2",
                    textAlign: "left",
                  }}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr
              key={row.id}
              style={{
                backgroundColor: row.getIsSelected() ? "#e6f7ff" : "white",
              }}
            >
              {row.getVisibleCells().map((cell) => (
                <td
                  key={cell.id}
                  style={{
                    padding: "8px",
                    border: "1px solid #ddd",
                  }}
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>

      {/* 選択された行の情報を表示 */}
      <div style={{ marginTop: "20px" }}>
        <h2>選択された行の情報</h2>
        <pre>{JSON.stringify(selectedRowsData, null, 2)}</pre>
      </div>
    </div>
  );
};

export default App;

実行結果

image.png

7. テーブルのカスタマイズ

テーブルのデザインや動作をより細かくカスタマイズする方法

TanStack TableはヘッドレスUIライブラリであるため、基本的な構造や機能は提供するものの、デザインや細かな動作は開発者が自由にカスタマイズできます。

ここでは、「テーブルの見た目をもっと自分のアプリケーションに合ったものにしたい」「特定のセルの内容を特別な形式で表示したい」 といった要望に応えるための、より詳細なカスタマイズ方法について解説します。

レンダリングの制御

TanStack Tableでは、ヘッダー、セル、フッターのレンダリングを細かく制御することができます。

  • セルのカスタマイズ
    • 列定義のcellオプションにReactコンポーネントやJSXを指定することで、各セルの表示内容を自由にカスタマイズできます。
    • これにより、データのフォーマット、アイコンの表示、ボタンの追加など、様々な表現が可能になります。
      • 例えば、年齢が30歳以上の場合は背景色を変えて表示したい場合、以下のようにcellオプションを定義できます。

    const columns = [
      //... 他の列定義
      columnHelper.accessor('age', {
        header: '年齢',
        cell: info => {
          const age = info.getValue();
          return <div style={{ backgroundColor: age >= 30? 'lightgray' : 'white' }}>{age}</div>;
        },
      }),
      //...
    ];
  • ヘッダーのカスタマイズ

    • 前述の通り、headerオプションでヘッダーの表示をカスタマイズできます。
    • ソート機能のUIを組み込んだり、アイコンを表示したりすることができます。
  • フッターのカスタマイズ

    • 列定義にはfooterオプションもあり、列のフッターに表示する内容を定義できます。
    • 例えば、数値データの合計値をフッターに表示するなどの用途が考えられます。

    const columns = [
      //...
      columnHelper.accessor('age', {
        header: '年齢',
        footer: info => info.column.id + ' 合計', // 例: 'age 合計' と表示
      }),
      //...
    ];
  • 行の展開
    • renderDetailPanelオプションを使うと、各行を展開して追加情報を表示する機能(アコーディオンのようなUI)を実装できます。
    • 例えば、社員情報テーブルで、各社員の詳細な職務経歴を展開して表示するといった使い方ができます。

    const table = useReactTable({
      data: data,
      columns: columns,
      getCoreRowModel: getCoreRowModel(),
      //... 他のRowModel
      renderDetailPanel: ({ row }) => (
        <div>
          詳細情報{row.original.detailedDescription}
        </div>
      ),
    });

独自の機能追加の方法

  • カスタムフック
    • 必要に応じて、独自のReactフックを作成し、useReactTableから返されるテーブルインスタンスを操作することで、独自の機能を追加できます。
      • 例えば、行をドラッグアンドドロップで並び替えられるようにする機能などを実装できます。
  • メタデータ
    • 列定義のmetaフィールドを利用して、列に関する追加情報を保持し、レンダリングや他の処理で利用できます。
      • 例えば、特定の列が編集可能かどうかを示すフラグをmetaに格納し、それに基づいてセルのレンダリングを変更するといった使い方が考えられます。
  • 外部状態管理との連携
    • TanStack Tableは内部状態を管理しますが、必要に応じてReduxやZustandなどの外部状態管理ライブラリと連携することも可能です。
    • これにより、アプリケーション全体の状態とテーブルの状態を統合的に管理できます。

ソースコード

ここまでのソースコードを提示します。


"use client";

import React, { useState, Fragment } from "react";
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  getExpandedRowModel, // 行展開のために追加
  useReactTable,
  Row, // 型付けのために追加
} from "@tanstack/react-table";

type Person = {
  firstName: string;
  lastName: string;
  age: number;
  visits: number;
  status: string;
  progress: number;
  detailedDescription?: string;
};

const defaultData: Person[] = [
  {
    firstName: "太郎",
    lastName: "田中",
    age: 28,
    visits: 100,
    status: "正常",
    progress: 50,
    detailedDescription:
      "彼は経験豊富な開発者です。ReactとTypeScriptが得意です。",
  },
  {
    firstName: "花子",
    lastName: "山田",
    age: 35,
    visits: 40,
    status: "警告",
    progress: 80,
  },
  {
    firstName: "健太",
    lastName: "佐藤",
    age: 22,
    visits: 20,
    status: "正常",
    progress: 10,
    detailedDescription: "新卒のジュニアエンジニア。学習意欲が高いです。",
  },
  {
    firstName: "美咲",
    lastName: "伊藤",
    age: 41,
    visits: 150,
    status: "危険",
    progress: 30,
    detailedDescription:
      "プロジェクトマネージャー。複数の大規模プロジェクトを経験。",
  },
];

const columnHelper = createColumnHelper<Person>();

const columns = [
  columnHelper.display({
    id: "expander",
    header: () => null,
    cell: ({ row }) =>
      // 詳細情報がある行のみ展開可能にする
      row.original.detailedDescription ? (
        <button
          onClick={() => row.toggleExpanded()}
          style={{ cursor: "pointer" }}
        >
          {row.getIsExpanded() ? "👇" : "👉"}{" "}
          {/* 展開状態に応じてアイコン変更 */}
        </button>
      ) : null,
    footer: () => null,
  }),

  columnHelper.accessor("firstName", {
    header: () => <span></span>,
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
  }),

  columnHelper.accessor("lastName", {
    header: "",
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
  }),

  columnHelper.accessor("age", {
    header: "年齢",
    cell: (info) => {
      const age = info.getValue();
      // 年齢が30歳以上なら背景色を変更
      const style: React.CSSProperties = {
        backgroundColor: age >= 30 ? "lightcoral" : "lightgreen",
        padding: "2px 5px",
        borderRadius: "4px",
        display: "inline-block",
        minWidth: "30px",
        textAlign: "center",
      };
      return <div style={style}>{age}</div>;
    },
    footer: (info) => info.column.id + " 合計",
  }),

  columnHelper.accessor("visits", {
    header: "訪問数",
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
  }),

  columnHelper.accessor("status", {
    header: "ステータス",
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
  }),

  columnHelper.accessor("progress", {
    header: "進捗",
    cell: (info) => info.getValue(),
    footer: (info) => info.column.id,
  }),
];

const App = () => {
  const [data, setData] = useState(() => [...defaultData]);
  const [expanded, setExpanded] = useState({}); // 行の展開状態を管理

  // 詳細パネルをレンダリングする関数
  const renderDetailPanel = (row: Row<Person>) => {
    // 詳細情報がある場合のみパネルを表示
    if (!row.original.detailedDescription) {
      return null;
    }
    return (
      <div
        style={{
          padding: "10px",
          backgroundColor: "#f0f0f0",
          border: "1px solid #ddd",
          margin: "5px 0",
        }}
      >
        <strong>詳細情報:</strong>
        <p style={{ margin: "5px 0 0 0" }}>
          {row.original.detailedDescription}
        </p>
      </div>
    );
  };

  const table = useReactTable({
    data,
    columns,
    state: {
      // テーブルに展開状態を渡す
      expanded,
    },
    onExpandedChange: setExpanded, // 展開状態が変更されたときに呼び出されるハンドラ
    getCoreRowModel: getCoreRowModel(),
    getExpandedRowModel: getExpandedRowModel(), // 行展開モデルを有効化
    // metaを使用して独自のプロパティを追加できる
    meta: {
      renderDetailPanel, // 詳細パネル描画関数をmetaに保存
    },
  });

  return (
    <div style={{ padding: "20px" }}>
      <h1>TanStack Table カスタマイズ例</h1>
      <table
        style={{
          borderCollapse: "collapse",
          width: "100%",
          border: "1px solid black",
        }}
      >
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{
                    border: "1px solid black",
                    padding: "8px",
                    textAlign: "left",
                  }}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            // 通常の行と詳細パネル行をグループ化
            <Fragment key={row.id}>
              <tr style={{ borderBottom: "1px solid #eee" }}>
                {row.getVisibleCells().map((cell) => (
                  <td
                    key={cell.id}
                    style={{ border: "1px solid black", padding: "8px" }}
                  >
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
              {/* 行が展開されている場合、詳細パネルを別の行として描画 */}
              {row.getIsExpanded() && (
                <tr>
                  {/* 詳細パネル用のセル。全列にまたがるようにcolSpanを設定 */}
                  <td colSpan={row.getVisibleCells().length}>
                    {(table.options.meta as any)?.renderDetailPanel(row)}
                  </td>
                </tr>
              )}
            </Fragment>
          ))}
        </tbody>
        <tfoot>
          {table.getFooterGroups().map((footerGroup) => (
            <tr key={footerGroup.id}>
              {footerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  style={{
                    border: "1px solid black",
                    padding: "8px",
                    textAlign: "left",
                  }}
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.footer,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </tfoot>
      </table>
    </div>
  );
};

export default App;

実行結果

image.png

まとめ

TanStack Tableを活用してReactで効率的なテーブルを作成しよう!

本稿を通じて、React初学者の方でもTanStack Tableの基本的な使い方から応用的なカスタマイズまでを理解し、ご自身のReactプロジェクトで効率的かつ柔軟なテーブル実装ができるようになることを目指しました。

TanStack Tableは、Reactで複雑なテーブル機能を実装するための強力で柔軟なライブラリです。
ヘッドレスUIであるため、デザインの自由度が高く、Material UIやshadcn/uiといった様々なUIライブラリと組み合わせることで、アプリケーションのデザインに完全に合致したテーブルを構築できます。

ソート、フィルタリング、ページネーションといった主要な機能が標準で提供されており、これらの機能を活用することで、効率的なデータ表示と操作を実現できます。
さらに、行選択機能や、セル、ヘッダー、フッターのレンダリングのカスタマイズ、独自の機能追加など、高度な要件にも対応できる拡張性も備えています。

Reactを学び始めたばかりの方が、本稿を参考に基本的な使い方から応用的なカスタマイズまでを習得し、TanStack Tableを活用してより高度なテーブルを実装できるようになる一助となれば幸いです。

学習リソース

TanStack Tableについてさらに深く学びたい場合は、以下のリソースが役立ちます。

これらのリソースを活用することで、TanStack Tableのより高度な機能や、具体的な実装例について学ぶことができます。


それでは良きTanStack Tableライフを!
ありがとうございました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?