この記事はReact Advent Calendar 2019の記事。

サービスにもよるとは思うが、業務系のサービスではテーブルUIはほぼ確実に登場する。

しかも、よく登場する上にたくさんの機能を求められるUIでもある。 パッと思いつく限りでも、ソート、フィルター、検索、ページネーション等多岐にわたる。

あり物のUIライブラリのテーブルComponentにオリジナルのスタイルを当てるという手もあるが、それだとUIライブラリの仕様に引っ張られたり、UIとロジックの分離がうまくできなかったりすることがある。

そこで、今回は「テーブルで必要になる機能」だけをHooksとし、UIとロジックを完全に分離できるreact-table v7を紹介してみる。

注意点 Link to heading

この記事執筆時点ではreact-table自体はRCになっているが、react-tableの型定義はまだマージされていない。

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40816

また、議論の末、現段階最新バージョンのrc.9ではreact-table側で型定義を管理しないことになっている。

そのため、この記事の紹介ではTypeScriptを使って紹介をする都合上、以下のコマンドで入るバージョンを使用して紹介する。

yarn add react-table@7.0.0-beta.23

大きく使い勝手が変わるようなことはないと思うが、react-tableは開発が活発なため、この記事の内容が陳腐化している可能性がある。

本番で使う際は必ず公式のドキュメントを参照するように。

シンプルなテーブル Link to heading

まずは最もシンプルにテーブルを表示する例。

import * as React from 'react';
import { render } from 'react-dom';

import { useTable, Column } from 'react-table';

import './styles.css';

const columns: Column<Data>[] = [
  {
    Header: '名前',
    accessor: 'name'
  },
  {
    Header: '年齢',
    accessor: 'age'
  }
];

interface Data {
  name: string;
  age: number;
}

const data: Data[] = [
  {
    name: 'John',
    age: 23
  },
  {
    name: 'Jane',
    age: 26
  }
];

function App() {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow
  } = useTable<Data>({ columns, data });

  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps()}>{column.render('Header')}</th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map((row, i) => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map(cell => {
                return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
              })}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

const rootElement = document.getElementById('root');
render(<App />, rootElement);

キモとなるのは以下の部分。

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow
  } = useTable<Data>({ columns, data });

useTable はColumnとそれに対応した情報 data を受け取って TableInstance を返す。

TableInstance はTableを描画するのに必要な情報をすべて持っており、Tableの内部で変更があったときに更新される。

描画に使っているTableタグは、完全にreact-tableからは独立していて、 TableInstance と組み合わせて使うことで完全に描画とロジックを独立させることが可能になっている。

Sortの機能を追加する Link to heading

上記例では特に複雑な機能がないため、テーブルを作る上でありがちなSortの機能を追加してみる。

まず、TypeScriptでreact-tableの機能を使うためには以下の型定義ファイル設置する必要がある。

import {
  UseSortByColumnOptions,
  UseSortByColumnProps,
  UseSortByInstanceProps,
  UseSortByOptions,
  UseSortByState
} from 'react-table';

declare module 'react-table' {
  export interface TableOptions<D extends object> extends UseSortByOptions<D> {}

  export interface TableInstance<D extends object = {}>
    extends UseSortByInstanceProps<D> {}

  export interface TableState<D extends object = {}>
    extends UseSortByState<D> {}

  export interface Column<D extends object = {}>
    extends UseSortByColumnOptions<D> {}

  export interface ColumnInstance<D extends object = {}>
    extends UseSortByColumnProps<D> {}
}

これはreact-tableがもともとJSベースで作られており、使いたい機能をプラグインとして使えるようにするという設計だったため、ライブラリ側で型定義を提供することが難しいため必要になっている(参考)。

この定義自体はコミュニティの型定義が進んだら変わるかもしれないので、公式を参照するように。

上記定義ができたら次はSortの実装する。

import * as React from 'react';
import { render } from 'react-dom';

import { useTable, Column, useSortBy } from 'react-table';

import './styles.css';

const columns: Column<Data>[] = [
  {
    Header: '名前',
    accessor: 'name'
  },
  {
    Header: '年齢',
    accessor: 'age'
  }
];

interface Data {
  name: string;
  age: number;
}

const data: Data[] = [
  {
    name: 'John',
    age: 23
  },
  {
    name: 'Jane',
    age: 26
  }
];

function App() {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow
  } = useTable<Data>({ columns, data }, useSortBy);

  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps(column.getSortByToggleProps())}>
                {column.render('Header')}
                <span>
                  {' '}
                  {column.isSorted
                    ? column.isSortedDesc
                      ? ' 🔽'
                      : ' 🔼'
                    : ''}{' '}
                </span>
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map((row, i) => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map(cell => {
                return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
              })}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

const rootElement = document.getElementById('root');
render(<App />, rootElement);

差分は以下の2箇所。

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow
  } = useTable<Data>({ columns, data }, useSortBy);
              <th {...column.getHeaderProps(column.getSortByToggleProps())}>
                {column.render('Header')}
                <span>
                  {' '}
                  {column.isSorted
                    ? column.isSortedDesc
                      ? ' 🔽'
                      : ' 🔼'
                    : ''}{' '}
                </span>
              </th>

useTableuseSortBy を渡し、 column に含まれた getSortByToggleProps を使って列をソート可能な状態にして、 isSortedisSortedDesc を利用して現在のソートの状態を描画するようにしている。

このパターンでも、基本的にはSortしたかどうかの状態はすべてreact-table側が握り、使う側は useTable の戻り値を利用して描画するだけでいい状態を維持できる。

フィルターや行選択といった機能も提供されているため、必要に応じて型定義ファイルを更新しつつプラグインを追加することで、自分のプロダクトで必要なテーブルの機能だけを使えるようになる。

一応、今回作成したコードは以下に設置しておく。

注意点その2 Link to heading

react-tableはHooksとして提供されているため、渡すときにメモ化されていないと正常に動作しない機能がある(自分も2回ほど引っかかった)。

公式のドキュメントにメモ化が必要なものについてはその旨が書いてあるので、自分が使いたいプラグインの部分は熟読することをオススメする。