WEB/システム/IT技術ブログ

TanStack TableのReact Tableでリッチなテーブルを実装してみる

TanStack TableのReact Tableでリッチなテーブルを実装してみます。
TanStack | High Quality Open-Source Software for Web Developers

あるプロジェクトで脱jQueryの流れがあり、jQueryベースのプラグイン「DataTables」に代わるライブラリを探していて、React Tableにたどり着きました。

本記事の内容は、ほとんど公式サイトのガイド通り、かつ、雑な解説となっておりますが、、初導入時の記憶としてメモを残したいと思います。

  • React 18.1
  • React Table 8.2.2

ここではユーザ情報ををTableで表示する、シンプルな例を取り上げます。
また、コード量が多くなるので、要点を絞り紹介しています。

React Tableのインストール

npmコマンドを使ってReact Tableをインストールします。

$ npm install @tanstack/react-table

React Tableを設置する

まずは、ベーシックとなるテーブルを表示してみます。
ライブラリを読み込みます。

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

データの型定義とデータを用意します。

type User = {
	firstName: string
	lastName: string
	age: number
}
const data: User[] = [
	{
		firstName: '太郎',
		lastName: '山田',
		age: 42,
	},
	{
		firstName: '花子',
		lastName: '山田',
		age: 30,
	},
	...
]

カラムを定義します。

	const columns: ColumnDef[] = [
		{
			accessorKey: 'firstName',
			header: '名',
		},
		{
			accessorKey: 'lastName',
			header: '姓',
		},
		{
			accessorKey: 'age',
			header: '年齢',
		},
	]

テーブルを生成します。

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

実際に表示するJSXの部分のコードになります。

  return (
    <>
      <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が表示されると思います。

ページャを設置する

データ数が数百、数千となると、テーブルが縦長になってしますので、ページングできるようにします。
ライブラリに「getPaginationRowModel」を追加します。

import {
	ColumnDef,
	flexRender,
	getCoreRowModel,
	useReactTable,
	getPaginationRowModel,
} from '@tanstack/react-table'

テーブルを生成する際に「getPaginationRowModel」を追加します。

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

JSXにページナビゲーション部分のHTMLを追記します。

      <div>
        <button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>
          {'<<'}
        </button>
        <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
          {'<'}
        </button>
        <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
          {'>'}
        </button>
        <button onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()}>
          {'>>'}
        </button>
        <span>
          <div>Page</div>
          <strong>
            {table.getState().pagination.pageIndex + 1} of{' '}
            {table.getPageCount()}
          </strong>
        </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)
            }}
          />
        </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>

以上でページングが可能になります。

テーブルをソートする

次に、氏名や年齢でテーブルの行を並べ替えます。
ライブラリに「getPaginationRowModel」と「SortingState」を追加します。

import {
	ColumnDef,
	flexRender,
	getCoreRowModel,
	useReactTable,
	getPaginationRowModel,
	getSortedRowModel,
	SortingState,
} from '@tanstack/react-table'

ソートの状態を管理する変数が必要となるので、以下のように宣言します。

	const [sorting, setSorting] = useState([])

テーブルを生成する際に、上記の変数と関数、そして「getSortedRowModel」を追加します。

	const table = useReactTable({
		data,
		columns,
		state: {
			sorting,
		},
		getCoreRowModel: getCoreRowModel(),
		getPaginationRowModel: getPaginationRowModel(),
		getSortedRowModel: getSortedRowModel(),
	})

JSXで、テーブルのヘッダ行にソートナビゲーション部分のHTMLを追記します。

    <thead>
      {table.getHeaderGroups().map(headerGroup => (
        <tr key={headerGroup.id}>
          {headerGroup.headers.map(header => {
            return (
              <th key={header.id} colSpan={header.colSpan}>
                {header.isPlaceholder ? null : (
                  <div
                    {...{
                      className: header.column.getCanSort()
                        ? 'cursor-pointer select-none'
                        : '',
                      onClick: header.column.getToggleSortingHandler(),
                    }}
                  >
                    {flexRender(
                      header.column.columnDef.header,
                      header.getContext()
                    )}
                    {{
                      asc: ' ▲',
                      desc: ' ▼',
                    }[header.column.getIsSorted() as string] ?? null}
                  </div>
                )}
              </th>
            )
          })}
        </tr>
      ))}
    </thead>

以上でソートが可能になります。
ヘッダのカラム名のエリアをクリックするとそのカラムでソートされ、「▲」や「▼」が表示されます。

テーブルをフィルタリングする

次に、各カラムをフリーワードで検索してフィルタリングします。
ライブラリに「Column」、「Table」と「getFilteredRowModel」を追加します。

import {
	ColumnDef,
	flexRender,
	getCoreRowModel,
	useReactTable,
	getPaginationRowModel,
	getSortedRowModel,
	SortingState,
	Column,
	Table,
	getFilteredRowModel,
} from '@tanstack/react-table'

テーブルを生成する際に「getFilteredRowModel」を追加します。

	const table = useReactTable({
		data,
		columns,
		state: {
			sorting,
		},
		getCoreRowModel: getCoreRowModel(),
		getPaginationRowModel: getPaginationRowModel(),
		getSortedRowModel: getSortedRowModel(),
		getFilteredRowModel: getFilteredRowModel(),
	})

JSXで、テーブルのヘッダ行にフィルタリングのフォームコンポーネント「Filter」を追加します。

    <thead>
      {table.getHeaderGroups().map(headerGroup => (
        <tr key={headerGroup.id}>
          {headerGroup.headers.map(header => {
            return (
              <th key={header.id} colSpan={header.colSpan}>
                {header.isPlaceholder ? null : (
                  <>
                    <div
                      {...{
                        className: header.column.getCanSort()
                          ? 'cursor-pointer select-none'
                          : '',
                        onClick: header.column.getToggleSortingHandler(),
                      }}
                    >
                      {flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                      {{
                        asc: ' ▲',
                        desc: ' ▼',
                      }[header.column.getIsSorted() as string] ?? null}
                    </div>
                    {header.column.getCanFilter() ? (
                      <div>
                        <Filter column={header.column} table={table} />
                      </div>
                    ) : null}
                  </>
                )}
              </th>
            )
          })}
        </tr>
      ))}
    </thead>

さらに、Filterコンポーネントを定義します。

const Filter = ({
    column,
    table,
  }:{
    column: Column<any, any>
    table: Table<any>
  }) => {
  const firstValue = table
    .getPreFilteredRowModel()
    .flatRows[0]?.getValue(column.id)

  return typeof firstValue === 'number' ? (
    <div>
      <input
        type="number"
        value={((column.getFilterValue() as any)?.[0] ?? '') as string}
        onChange={e =>
          column.setFilterValue((old: any) => [e.target.value, old?.[1]])
        }
        placeholder={`Min`}
      />
      <input
        type="number"
        value={((column.getFilterValue() as any)?.[1] ?? '') as string}
        onChange={e =>
          column.setFilterValue((old: any) => [old?.[0], e.target.value])
        }
        placeholder={`Max`}
      />
    </div>
  ) : (
    <input
      type="text"
      value={(column.getFilterValue() ?? '') as string}
      onChange={e => column.setFilterValue(e.target.value)}
      placeholder={`Search...`}
    />
  )
}

ちなみに、フィルタリングのフォームは、カラム定義でオプション「accessorKey」を設定すると自動で表示されますが、オプション「enableColumnFilter」を追加して非表示設定することもできます。

	const columns: ColumnDef[] = [
		{
			accessorKey: 'firstName',
			header: '名',
			enableColumnFilter: false,
		},
		(中略)

公式サイトのサンプルでは動作しないものもあり、導入するには試行錯誤しながら、結構な時間を要しました。
jQueryでは数行追記すれば高機能なテーブルを簡単に実装できていたのですが、Reactでは、比較的多くの時間とコードを費やしても、実現できる機能は十分ではありません。
ライブラリ移行は慎重なほうがよいかもしれません。

B!

Comment

コメントはありません

コメントする

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Monthly Archives