[React] React Table で テーブル内のファジー(あいまい)検索 をしてみる | 心を無にして始める React

準備

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

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

検索

イメージ

シンプルな検索

検索用の入力フォームに文字を入力されると、検索を実行して結果を表示します。
検索方法には「入力された文字列が含まれているか」を採用します。

また、検索用の入力フォームに文字を入力して 0.5秒間 変化がなければ、検索を実行するようにします。
(入力中はすぐに検索せず、待機させます。)

今回は 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';

const filter = (row, columnId, value) => {
  return String(row.getValue(columnId)).indexOf(value) !== -1;
}

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: filter,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getSortedRowModel: getSortedRowModel(),
  })

  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())
                        }
                      </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 は 前の記事 と同じです。

少しだけカスタマイズ

さすがに、このままだと使えないので、ある程度使える形にします (*’▽’)

  • スペース区切りは OR 条件として検索したい
  • 大文字小文字は区別したくない

filter 関数を編集します (/・ω・)/

const filter = (row, columnId, value) => {
  const values = value.split(/\s+/).map(x => x.toLowerCase());
  const cellText = String(row.getValue(columnId)).toLowerCase();
  
  return values.some(value => cellText.indexOf(value) !== -1);
}

スペース区切りを OR条件

ファジーな検索

検索方法を「あいまい検索」に変えます。

準備

追加でインストールするものがあります。

npm install @tanstack/match-sorter-utils

局所的なサンプル

filter 用の関数をファジー検索に対応した形にします。

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;
}

ぜんたい

シンプルな検索の 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())
                        }
                      </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;

はい、できました。