[React] React Table で ソート をしてみる | 心を無にして始める React

今回は、前回の ファジー検索 の機能を付けたものに ソート の機能を加えていきます。

準備

TanStack Table v8 で React Table を使うのはこちら (*’▽’)

バックエンドには、いつもの json-server を使います。

ソート

イメージ

局所的なサンプル

ソートできる場合にはマウスカーソルを 指のマーク に変更して、ソートの状態を ▲▼ で表現しています。

TanStack Table はカスタマイズの自由度が高い分、なんとなく使い方は難し目な気がしてきました 💦

{
  header.isPlaceholder
    ? null
    : (
      <div
        {...{
          style: {
            cursor: header.column.getCanSort()
              ? 'pointer'
              : '',
          },
          onClick: header.column.getToggleSortingHandler(),
        }}
      >
        {flexRender(header.column.columnDef.header, header.getContext())}
        {
          {
            asc: <span className="ps-3" style={{ fontSize: '50%' }}>▲<span style={{ color: '#333' }}>▼</span></span>,
            desc: <span className="ps-3" style={{ fontSize: '50%' }}><span style={{ color: '#333' }}>▲</span>▼</span>,
          }[header.column.getIsSorted()]
          ?? <span className="ps-3" style={{ fontSize: '50%', color: '#333' }}>▲▼</span>
        }
      </div>
    )
}

サンプル

Table.js がこちら。

import React, { useEffect, useState } from 'react';
import { Table as BootstrapTable } from 'react-bootstrap';
import { useReactTable, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel } from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';

const fuzzyFilter = (row, columnId, value, addMeta) => {
  const itemRank = rankItem(row.getValue(columnId), value);
  addMeta({
    itemRank,
  })
  return itemRank.passed;
}

const DebouncedInput = ({
  value: initialValue,
  onChange,
  debounce = 500,
  ...props
}) => {
  const [value, setValue] = useState(initialValue)

  useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  useEffect(() => {
    const timeout = setTimeout(() => onChange(value), debounce);
    return () => clearTimeout(timeout)
  }, [value])

  return (
    <input {...props} value={value} onChange={e => setValue(e.target.value)} />
  )
}

const Table = React.forwardRef(({
  columns,
  rows,
  ...otherProps
}, ref) => {

  const [globalFilter, setGlobalFilter] = useState('')

  const table = useReactTable({
    columns,
    data: rows,
    state: {
      globalFilter,
    },
    onGlobalFilterChange: setGlobalFilter,
    globalFilterFn: fuzzyFilter,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getSortedRowModel: getSortedRowModel(),
    debugTable: true,
  })

  return (
    <div className="w-100">
      <div className="mb-2 w-100" style={{ paddingLeft: 1, paddingRight: 1 }}>
        <DebouncedInput
          value={globalFilter ?? ''}
          onChange={value => setGlobalFilter(String(value))}
          className="w-100 p-2 font-lg shadow border border-block"
          placeholder="検索"
        />
      </div>

      <BootstrapTable ref={ref} {...otherProps}>
        <thead>
          {
            table.getHeaderGroups().map(headerGroup => (
              <tr key={headerGroup.id}>
                {
                  headerGroup.headers.map(header => {
                    return (
                      <th key={header.id}>
                        {/* {
                          header.isPlaceholder
                            ? null
                            : flexRender(header.column.columnDef.header, header.getContext())
                        } */}
                        {
                          header.isPlaceholder
                            ? null
                            : (
                              <div
                                {...{
                                  style: {
                                    cursor: header.column.getCanSort()
                                      ? 'pointer'
                                      : '',
                                  },
                                  onClick: header.column.getToggleSortingHandler(),
                                }}
                              >
                                {flexRender(header.column.columnDef.header, header.getContext())}
                                {
                                  {
                                    asc: <span className="ps-3" style={{ fontSize: '50%' }}>▲<span style={{ color: '#333' }}>▼</span></span>,
                                    desc: <span className="ps-3" style={{ fontSize: '50%' }}><span style={{ color: '#333' }}>▲</span>▼</span>,
                                  }[header.column.getIsSorted()]
                                  ?? <span className="ps-3" style={{ fontSize: '50%', color: '#333' }}>▲▼</span>
                                }
                              </div>
                            )
                        }
                      </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>
      </BootstrapTable>
    </div>
  )
})

export default Table;

App.js は特に変更ありませんが、ぺたり。

import axios from 'axios';
import { Suspense, useState } from 'react';
import { Spinner } from 'react-bootstrap';

import './App.css';
import Table from './components/Table';

axios.defaults.headers.get['Content-Type'] = 'application/json';
axios.defaults.headers.get.Accept = 'application/json';
axios.defaults.baseURL = 'http://localhost:3000/';

const COLUMNS = [
  {
    accessorKey: 'id',
    header: 'ID',
  },
  {
    accessorKey: 'name',
    header: 'なまえ',
  },
];

const Pets = ({
  values,
  setState,
}) => {

  if (values === null) {
    if (setState) {
      throw axios.get('/cats').then(response => setState(response.data));
    }
  }

  return (
    <Table
      bordered
      hover
      striped
      variant="dark"
      columns={COLUMNS}
      rows={values}
    />
  );
};

function App() {

  const [cats, setCats] = useState(null);

  return (
    <div className="App">
      <header className="App-header p-5">
        <Suspense fallback={<Spinner animation="border" variant="light" />}>
          <Pets values={cats} setState={setCats} />
        </Suspense>
      </header>
    </div>
  );
}

export default App;

はい、できました。