Kaito Suzuki

Kaito Suzuki

Software Engineer

Tanstack Table の 設計とTips

Tanstack Table の 設計とTips

自己紹介

2022年に中途入社した鈴木海斗です。フロントエンドやアルゴリズム開発を中心におこなっています。本記事ではフロントエンド開発でテーブルを作る時に利用可能なライブラリの一つである Tanstack Table の基本的な使い方を紹介していきます。

対象読者

フロントエンド開発で Tanstack Table を導入しようとしている方。

ドキュメントのゴール

Tanstack Table に関する以下の項目について基本的な使い方を説明します。

  • テーブルの表示
  • ソート
  • 行の更新、追加
  • カラムの表示順、ピン

Tanstack Table とは

Tanstack Table は TypeScript/JavaScript、Angular、Lit、React、Vue、Solid、Qwik、Svelte 向けのテーブルを構築するための Headless UI ライブラリです。Headless UI とはスタイルを提供せず、ロジックや状態のみを提供することで独自のUIを作成可能なものを指します。Headless UI ライブラリに対してデザインも含めて事前構築されたコンポーネントを提供する UIコンポーネントライブラリがあり、例えば Tanstack Table を元にデザインを当てたライブラリとして Material React Table などが存在します。Headless UI であることで自由にスタイリングを行うことが可能になる一方、その分の工数が必要になるため用途に応じて選択することになります。

環境

本記事では主要ライブラリが以下のバージョンでの Next.js を用いた実装例を紹介します。TypeScript など他に利用しているライブラリがありますが記載は割愛しています。

  • next: 13.4.7
  • react: 18.2.0
  • @tanstack/react-table: 8.9.3

テーブルの表示

ここからは React を利用した際における Tanstack Table を用いた実装例を紹介していきます。
Tanstack Table は Headless UI ライブラリなので、ロジック部分の実装に加えて UI に関する実装を CSS や他の UI コンポーネントライブラリを用いて別でコーディングする必要があります。 例として、hogefuga をカラムに持つ基本的なテーブルの実装例を紹介します。ロジックを /app/page.tsx、UI を /components/ShowTable.tsx に実装するものとします。
以下のコードが /app/page.tsx の実装です。

import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';

// テーブルデータの型を定義
type TestTable = {
    hoge: string;
    fuga: number;
}

// createColumnHelper を利用してカラム定義を作成
const columnHelper = createColumnHelper<TestTable>();

const testTableColumnDefs = [
    columnHelper.accessor((row) => row.hoge, {
        id: 'hoge',
        header: 'hoge',
    }),
    columnHelper.accessor((row) => row.fuga, {
        id: 'fuga',
        header: 'fuga',
    }),
];

export default function Page() {
    const [data, setData] = useState<TestTable[]>([
        { hoge: 'hoge1', fuga: 0 },
        { hoge: 'hoge2', fuga: 1 },
        { hoge: 'hoge3', fuga: 2 },
    ]);

    // useReactTable 呼び出し
    const table = useReactTable<TestTable>({
        columns: testTableColumnDefs,
        data: data,
        getCoreRowModel: getCoreRowModel(),
    })

    return <ShowTable table={table} />
}

流れを大まかに説明すると以下のようになります。

  • テーブルデータの型を定義
  • createColumnHelper を利用してカラム定義を作成
  • useReactTable 呼び出し

最低限のUI部分のコンポーネント実装例が以下のコードです。

import {
  Cell,
  Header,
    Table, flexRender,
  } from '@tanstack/react-table';


const ShowHeader = ({
  table,
}:{
  table: Table<any>;
}) => {
  return (
    <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>
  )
}

const ShowBody = ({
  table,
}:{
  table: Table<any>;
}) => {
  return (
    <tbody>
      {table.getRowModel().rows.map((row, index) => {
        const tableCell = (cell: Cell<any, unknown>, cellIndex?: number) => (
          <td key={cell.column.id}>
            {flexRender(cell.column.columnDef.cell, cell.getContext())}
          </td>
        );
        return (
          <tr
            key={index}
            style={{ textAlign: 'center' }}
          >
            <>{row.getVisibleCells().map(tableCell)}</> 
          </tr>
        );
      })}
    </tbody>
  )
}

export const ShowTable = ({
    table
}:{
    table: Table<any>;
}
) => {
    return (
        <div>
          <main>
            <table>
              <ShowHeader table={table} />
              <ShowBody table={table} />
            </table>
          </main>
        </div>
      );
}

Table を引数とするこのコンポーネントの実装によってUIのカスタマイズが可能になります。 上記実装を行ったものが以下の画像です。

Tableを引数とするコンポーネントの実装の画像

ソート

useReactTable の引数に getSortedRowModel() を渡すことで、クライアント側でのソート機能を実現できます。 ソートの状態は SortingState で表し、配列の先頭の要素から順にキーとしてソートが行われます。
また、比較関数をカラム定義で sortingFn に渡すことで変更することができ、デフォルトで用意されたものがいくつかありますが自作することも可能です。 hoge カラムにデフォルト比較関数の alphanumericfuga カラムに undefined が混ざっている時の挙動を定義した自作の比較関数で、fuga を第一ソートキーで降順、hoge を第二ソートキーで昇順としたソート機能を持たせる実装例を以下に示します。

import { SortingState, createColumnHelper, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';


type TestTable = {
    hoge: string;
    fuga: number | undefined;
}

const columnHelper = createColumnHelper<TestTable>();

const testTableColumnDefs = [
    columnHelper.accessor((row) => row.hoge, {
        id: 'hoge',
        header: 'hoge',
        sortingFn: 'alphanumeric' // 英数字用の比較関数
    }),
    columnHelper.accessor((row) => row.fuga, {
        id: 'fuga',
        header: 'fuga',
        sortingFn: (a, b, columnId) => { // undefinedを最も大きいとする自作比較関数
            if(a.original.fuga == undefined && b.original.fuga == undefined){
                return 0;
            }
            if(a.original.fuga == undefined){
                return 1;
            }
            if(b.original.fuga == undefined){
                return -1;
            }
            return a.original.fuga - b.original.fuga;
        }
    }),
];

export default function Page() {
    const [data, setData] = useState<TestTable[]>([
        { hoge: '1', fuga: 0 },
        { hoge: '3', fuga: 2 },
        { hoge: '2', fuga: 2 },
        { hoge: '4', fuga: undefined },
    ]);
    
    // 第一ソートキーをfugaで降順、第二ソートキーをhogeで昇順としている
    const [sorting, setSorting] = useState<SortingState>([{ id: 'fuga', desc: true}, { id: 'hoge', desc: false}]);

    const table = useReactTable<TestTable>({
        columns: testTableColumnDefs,
        data: data,
        getCoreRowModel: getCoreRowModel(),
        getSortedRowModel: getSortedRowModel(), // クライアント側でのソートを行いたい場合引数に渡す
        state: {
            sorting: sorting,
        },
        onSortingChange: setSorting,
    })

    return <ShowTable table={table} />
}

上記実装を行ったものが以下の画像です。

ソート機能の実現

参考: https://tanstack.com/table/latest/docs/guide/sorting#sorting-guide

行の更新、追加

Cell への入力に基づいてテーブルデータを動的に更新したいことがあると思います。 Tanstack Table ではuseReactTableの引数である meta にメソッドを渡すことでCell コンポーネント内から cell.table.options.meta としてアクセスできます。 これを利用することで行の更新や追加を行うことができます。
以下のコードは、行の更新、追加、複製機能を持ったテーブルの実装例です。

import { RowData, createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';

// metaに渡す型を宣言
declare module '@tanstack/react-table' {
    interface TableMeta<TData extends RowData> {
        addRow: (row: TData) => void;
        updateRow: (index: number, row: TData) => void;
    }
}

type TestTable = {
    hoge: string;
    fuga: number;
}

const columnHelper = createColumnHelper<TestTable>();

const testTableColumnDefs = [
    columnHelper.accessor((row) => row.hoge, {
        id: 'hoge',
        header: 'hoge',
    }),
    columnHelper.accessor((row) => row.fuga, {
        id: 'fuga',
        header: 'fuga',
    }),
    columnHelper.display({ // 同じ行を更新するカラム
        id: 'update',
        header: '更新',
        cell: (cell) => {
            return <button onClick={() => {
                cell.table.options.meta?.updateRow(cell.row.index, { hoge: 'updated', fuga: cell.row.getValue('fuga') as number + 1});
            }
            }></button>
        }
    }),
    columnHelper.display({ // 複製を行うカラム
        id: 'duplicate',
        header: '複製',
        cell: (cell) => {
            return <button onClick={() => {
                cell.table.options.meta?.addRow(cell.row.original);
            }
            }></button>
        }
    }),
];

export default function Page() {
    const [data, setData] = useState<TestTable[]>([
        { hoge: 'hoge', fuga: 0 },
        { hoge: 'hoge', fuga: 1 },
        { hoge: 'hoge', fuga: 2 },
    ]);

    const table = useReactTable<TestTable>({
        columns: testTableColumnDefs,
        data: data,
        getCoreRowModel: getCoreRowModel(),
        meta: { // talble.options.meta からアクセスしたいものを渡す
            addRow: (row: TestTable) => {
                setData([...data, row]);
            },
            updateRow: (index: number, row: TestTable) => {
                setData(data.map((d, i) => i === index ? row : d));
            }
        }
    })

    return <>
        <ShowTable table={table} />
        <button onClick={() => { // 新しい行を追加するボタン
            table.options.meta?.addRow({ hoge: 'new', fuga: table.getRowModel().rows.length});
        }
        }></button>
    </>
}

上記実装を行ったものが以下の画像です。

参考: https://tanstack.com/table/latest/docs/api/framework/react/examples/editable-data

カラムの表示順、ピン

例えばユーザーごとにカラムの表示順を変えたいときや、一部のカラムをピンしたい時があります。 このような時にも Tanstack Table の機能で対応することが可能です。 表示順やピンのステータスを useState で宣言し、それを useReactTable の引数に渡すことで、変更することができます。
以下がデフォルトでカラムの表示順を設定し、ヘッダーに動的にピンを設定できるUIを追加した実装例です。


import { Column, ColumnPinningState, createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';
import { RiPushpin2Fill } from "react-icons/ri";


type TestTable = {
    hoge: string;
    fuga: number;
}

const columnHelper = createColumnHelper<TestTable>();

// ピンをオンオフできるボタン
const PinToggleButton = <T,>({ column }: { column: Column<T, unknown>}) => {
    if(!column.getCanPin()){
        return null;
    }
    return <button onClick={() => {
        if(column.getIsPinned() === 'left'){
            column.pin(false);
        }else{
            column.pin('left');
        }
    }}><RiPushpin2Fill /></button>
}

const testTableColumnDefs = [
    columnHelper.accessor((row) => row.hoge, {
        id: 'hoge',
        header: (header) => { // ヘッダーにピンを設定できるボタンを表示
            return <>
                hoge
                <PinToggleButton column={header.column} />
            </>
        },
    }),
    columnHelper.accessor((row) => row.fuga, {
        id: 'fuga',
        header: (header) => {
            return <>
                fuga
                <PinToggleButton column={header.column} />
            </>
        }
    }),
];

export default function Page() {
    const [data, setData] = useState<TestTable[]>([
        { hoge: 'hoge1', fuga: 0 },
        { hoge: 'hoge2', fuga: 1 },
        { hoge: 'hoge3', fuga: 2 },
    ]);
    const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({ left: [], right: []}); // ピンの設定
    const [columnOrder, setColumnOrder] = useState<string[]>(['hoge', 'fuga']); // カラムの表示順設定

    // state と onColumnPinningChange、onColumnOrderChange に必要なものを渡す
    const table = useReactTable<TestTable>({
        columns: testTableColumnDefs,
        data: data,
        getCoreRowModel: getCoreRowModel(),
        state: {
            columnPinning: columnPinning,
            columnOrder: columnOrder,
        },
        onColumnPinningChange: setColumnPinning,
        onColumnOrderChange: setColumnOrder,
    })

    return <>
        <ShowTable table={table} />
        <button onClick={() => { // 表示順を後から変更するボタン
            setColumnOrder(['fuga', 'hoge']);
        }
        }>Change Column Order</button>
    </>
}

上記実装を行ったものが以下の画像です。

参考: https://tanstack.com/table/latest/docs/guide/column-ordering#column-ordering-guide    https://tanstack.com/table/latest/docs/guide/column-pinning#column-pinning-guide

その他

テーブルデータのうちいくつかのカラムについては中身が空であるというケースはよくあると思います。 そのようなデータをどのように表示するかは acccesorcell にカラムごとに定義することもできますが、 useReactTable の引数に renderFallbackValue を設定することでデータが空の時の表示を一律に設定することが可能です。 以下がデータに undefined が混ざっている時に renderFallbackValue を設定した実装例です。

import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { ShowTable } from '@/components/ShowTable';
import { useState } from 'react';

type TestTable = {
    hoge: string;
    fuga: number | undefined; // undefinedが入ることがある
}

const columnHelper = createColumnHelper<TestTable>();

const testTableColumnDefs = [
    columnHelper.accessor((row) => row.hoge, {
        id: 'hoge',
        header: 'hoge',
    }),
    columnHelper.accessor((row) => row.fuga, {
        id: 'fuga',
        header: 'fuga',
    }),
];

export default function Page() {
    const [data, setData] = useState<TestTable[]>([
        { hoge: 'hoge1', fuga: undefined },
        { hoge: 'hoge2', fuga: 1 },
        { hoge: 'hoge3', fuga: undefined },
    ]);

    const table = useReactTable<TestTable>({
        columns: testTableColumnDefs,
        data: data,
        getCoreRowModel: getCoreRowModel(),
        renderFallbackValue: '-', // データがない時デフォルトで表示したい値を設定
    })

    return <ShowTable table={table} />
}

上記実装を行ったものが以下の画像です。

失敗談

プロジェクトで使っているテーブルに共通して行いたい設定などがある場合に、useReactTable をラップしたコンポーネントを作ることになると思います。 Tanstack Tableではページネーションを行うための機能もあり、ラップしたコンポーネント内でページネーションのデフォルトサイズを決めるような実装をしていたことにより、ページネーションを想定していない他のテーブルで表示される行数に制限が発生するというバグを生んでしまったことがあります。
どこまでの設定をデフォルトで行うかという点には注意が必要です。

まとめ

Tanstack Table の機能を具体例と併せていくつか紹介していきました。 実際にプロジェクトで使用してソートやフィルターなど様々な機能を簡単に実装できると感じています。 ここで紹介しきれていない機能もまだまだたくさんあるので、ぜひ公式ドキュメントも合わせて Tanstack Table を使ってみてください。

参考文献

Tanstack Table 公式ドキュメント



エムシーデジタルでは、技術力向上のためのイベントや勉強会なども定期的に実施しています。もしエムシーデジタルでのキャリアに興味を持っていただいた方がいらっしゃいましたら、まずはカジュアルな面談から実施することも可能です。お気軽にお声掛けください!
採用情報や面談申込みはこちらから
RSS

Tags

Previous

Keita Murase

Keita Murase

Cloud Tasks をローカルで動かす

はじめに ソフトウェアエンジニアの村瀬です。本記事では、Cloud Tasks や Cloud Storage といった GCP サービスをローカルでの動作検証の際に利用する方法を説明していきます。 背景 Cloud Tasks とは Cloud Tasks は Google Cloud Plat

  • #TechBlog
  • #CloudTask

Next

Shinsaku Segawa

Shinsaku Segawa

MCD Data Science Competition 開催の振り返り

はじめに データサイエンティストの瀬川です。少し前になりますが、2023年12月18日(月)にオフラインで主催した MCD Data Science Competition の振り返りをまとめたいと思います。 開催概要 MCD Data Science Compet

  • #TechBlog
  • #Kaggle