Hooks対応したreact-tableをTypeScriptで使う
この記事はReact Advent Calendar 2019の記事です。
みなさまテーブルComponentは作成していますか?
サービスにもよるとは思いますが、業務系のサービスではほぼ確実に登場するUIです。
しかも、よく登場する上にたくさんの機能を求められるUIでもあります。パッと思いつく限りでも、ソート、フィルター、検索、ページネーション等多岐にわたります。
これらの機能はUIと分離して1から作るは大変です。あり物のUIライブラリのテーブルComponentにオリジナルのスタイルを当てるという手もありますが、それだとUIライブラリの仕様に引っ張られたり、UIとロジックの分離がうまくできなかったりすることがあります。
そこで、今回は「テーブルで必要になる機能」だけをHooksとし、UIとロジックを完全に分離できるるreact-table v7を紹介しようと思います。
注意点
この記事執筆時点では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は開発が活発なため、この記事の内容が陳腐化している可能性があります。
本番で使う際は必ず公式のドキュメントを参照してください。
シンプルなテーブル
まずは最もシンプルにテーブルを表示する例です。
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の機能を追加する
上記例では特に複雑な機能がないため、テーブルを作る上でありがちな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>
useTable
に useSortBy
を渡し、 column
に新たに含まれた getSortByToggleProps
を使って列をソート可能な状態にして、 isSorted
と isSortedDesc
を利用して現在のソートの状態を描画するようにしています。
このパターンでも、基本的にはSortしたかどうかの状態はすべてreact-table側が握り、使う側は useTable
の戻り値を利用して描画するだけでいい状態を維持することができます。
フィルターや行選択といった機能も提供されているため、必要に応じて型定義ファイルを更新しつつプラグインを追加することで、自分のプロダクトで必要なテーブルの機能だけを使えるようになります。
一応、今回作成したコードはいかに設置しておきます。
注意点その2
react-tableはHooksとして提供されているため、渡すときにメモ化されていないと正常に動作しない機能があります(自分も2回ほど引っかかった)。
公式のドキュメントにメモ化が必要なものについてはその旨が書いてあるので、自分が使いたいプラグインの部分は熟読することをおすすめします。