Tableでちょっと複雑なことをやりたかった
テーブルの中にボタンを入れた上でソートしたり、チェックボックス付けたり。
こんなようなものを作りたかった。意外と大変だった。
今回使ったのは、React Table。
これを Next.js + TypeScript に組み込みたい。
サンプルコードが豊富なので、説明が足らないような気もするが、やりたいことはだいたい何でもできる。Sugeee! が、TypeScript と相性がいまいちよろしくない。map した時に key がないので Warning が出まくる。など、思いの外使いにくかった。
最終的なコードはこちら。
kurab/next-typescript-react-table
準備
yarn create next-app next-typescript-react-table --typescript
cd next-typescript-react-table
yarn add react-table
yarn add @types/react-table --dev
構成の概要
├── components
│ ├── primaryButton.tsx
│ ├── SortableTable.tsx
│ └── SortableTableWithRowSelect.tsx
├── pages
│ ├── _app.tsx
│ └── index.tsx
├── types
│ ├── DataType.ts
│ ├── react-table.d.ts
│ └── react.d.ts
└── package.json
コードを全部書くと長くなるので、要点だけ。
TypeScript 対応
まず、react-table を TypeScript で使うには、型定義をする必要がある。これをしないと、TypeScript で React Table は使えない。
types/react-table.d.ts
がそれだが、今回面倒だったので、全ての型定義をこちらからコピペした。
が、使うものだけで良い。今回の場合、Sort と RowSelect 関連のものだけで良い。
Type エラーはまだまだ出るが、全体的な TypeScript 対応はいったんこれだけ。
Sort 可能な Table
基本的にはこのサンプル通りで良い。これを component にしたものが、components/SortableTable.tsx
だが、サンプルコードのままだと、key がないので、 Warning が出まくる。
key の設定方法は、以下を参考に作った。
thead
の一部だけを抜き取ると、
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
...
これがサンプル。key を組み込んだものが
<thead>
{headerGroups.map((headerGroup) => {
const { key, ...restHeaderGroupProps } =
headerGroup.getHeaderGroupProps();
return (
<tr {...restHeaderGroupProps} key={key}>
...
こんな感じで key を作っていく。th
の中に span
があるが、span
に key
を与えても Warning は消えないので、th
の中身は <></>
で囲う。
Table の data として string 以外は渡せるのか?
渡せる。
const onClickAlert = (name: string) => {
alert(name);
};
const dataButton: Array<DataButton> = useMemo(
() => [
{
col1: 'Hello',
col2: <PrimaryButton name={'hello'} callback={onClickAlert} />,
},
{...},
],
[]
);
こんな感じで、割と何でも渡せる。Type は必要なものを必要なように設定。
Sort 可能なチェックボックス付きの Table
チェックボックス付きはサンプルコードをもとに作る。ソートも付けたいので、html の部分は、前述のコードをそのまま使う。それに加え、今回のサンプルコードで、 useTable
を初期化する際に、useSortBy
を付けるだけで良い。
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
selectedFlatRows,
state: { selectedRowIds },
} = useTable(
{
columns,
data,
},
useRowSelect,
hooks => {
hooks.visibleColumns.push(columns => [
{
id: 'selection',
Header: ({ getToggleAllRowsSelectedProps }) => (
<div>
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
</div>
),
Cell: ({ row }) => (
<div>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
},
...columns,
])
サンプルコードでやっているのは、hooks で、チェックボックスをくっつけている。
ここで問題が2つ発生する。
まず、これが動かない。
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef()
const resolvedRef = ref || defaultRef
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
)
}
)
解決方法は、こちら。
賢い人がいるもんだ!stackoverflow の方をそのまま使った。
次に、
Cell: ({ row }) => (...)
ここでも Type エラーが出る。Binding element 'any' implicitly has an 'any' type.
そのままでも使えるが、build はできない。で、
Cell: ({ row: any }) => (...)
これだと動かない。
こちらを参考に
Cell: ({ row }: { row: any }) => (...)
とすることで、動くようになる。
あとは、コンポーネント間で select したものをやりとりしたいので、親コンポーネントから子コンポーネントに callback を渡して出来上がり。
完成
チェック入れた状態でソート順を変更しても、欲しい値はきちんと取れている。Pagenation とかもやりたいところだが、いまいま必要ないので、必要になった時に。
もともとの要件としては、csv をアップロードして、それを sort したりしながら見て、選んだものだけを DB に登録するという内容だったが、そういうのはエクセルで済ませてからアップロードして欲しいものだ。
おわり。