大量データのCSVを高速でテーブル表示する

2023年4月10日月曜日

react

t f B! P L

Reactで大量データのCSVを読み込んで、高速にテーブル表示する方法を紹介します。
今回紹介する方法であれば、たとえ10万行のCSVとかでも、数秒で画面に一覧表示できます。

準備

今回、以下のライブラリを使います。

  • Papa Parse:CSVをパースするライブラリ
  • React Window:大量データのスクロール表示を仮想化するライブラリ
  • React Table:ReactでTable表示を便利にするライブラリ(たぶん)

では、それぞれインストールします。

npm install papaparse --save
npm install react-window --save
npm install react-table --save

TypeScriptの人は、型の定義もインストールします。

npm install --save @types/papaparse
npm install --save @types/react-window
npm install --save @types/react-table

Tableコンポーネントの実装

まず、React WindowとReact Tableを組み合わせて、スクロールを仮想化した Table コンポーネントを作成します。

以下が完成形のコードです。

import React, { useRef } from 'react';
import { Column, useBlockLayout, useTable } from 'react-table';
import { VariableSizeGrid, GridOnScrollProps } from 'react-window';
import css from './Style.module.css';

/**
 * スクロールバーの幅を取得する関数
 * @returns すクローバーの幅(px)
 */
const scrollbarWidth = () => {
  const scrollDiv = document.createElement('div')
  scrollDiv.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;')
  document.body.appendChild(scrollDiv)
  const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
  document.body.removeChild(scrollDiv)
  return scrollbarWidth
}

/**
 * プロパティのインターフェイス
 */
interface TableProp {
  columns: Column<object>[]
  data: any[]
  width: number
}

/**
 * テーブルコンポーネント
 * @param TableProp 引数
 * @returns コンポーネント
 */
function Table({
  columns,
  data,
  width,
}: TableProp) {
  const defaultColumn = React.useMemo(
    () => ({
      width: 150,
    }),
    []
  )
  const refTHead = useRef<HTMLDivElement>(null)
  const scrollBarSize = React.useMemo(() => scrollbarWidth(), [])

  //react-tableの定義
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
  } = useTable(
    {
      columns,
      data,
      defaultColumn,
    },
    useBlockLayout
  )

  //ヘッダと明細部のスクロール同期
  const handleScroll = ({ scrollLeft }: GridOnScrollProps) => {
    if (refTHead.current) refTHead.current.scrollLeft = scrollLeft
  }

  //セルの描画
  const Cell = ({ columnIndex, rowIndex, style }: any) => {
    const row = rows[rowIndex]
    prepareRow(row) 
    return (
      <div style={style} className={css.td}>
        {row.cells[columnIndex].render('Cell')}
      </div>
    )
  };

  return (
    <div {...getTableProps()}
      className={css.table}
      style={{ width: `${width}px` }}>
      <div className={css.thead_wrapper}>
        <div
          ref={refTHead}
          className={css.thead}
          style={{ width: `${width - scrollBarSize -2}px` }}>
          {headerGroups.map(headerGroup => (
            <div {...headerGroup.getHeaderGroupProps()} className={css.tr}>
              {headerGroup.headers.map(column => (
                <div {...column.getHeaderProps()} className={css.th}>
                  {column.render('Header')}
                </div>
              ))}
            </div>
          ))}
        </div>
      </div>
      <div {...getTableBodyProps()}>
        <VariableSizeGrid
          columnCount={columns.length}
          columnWidth={i => parseInt((columns[i].width ?? 100) as any)}
          height={400}
          rowCount={rows.length}
          rowHeight={row => 35}
          width={width}
          className={css.tbody}
          onScroll={handleScroll}
        >
          {Cell}
        </VariableSizeGrid>
      </div>
    </div>
  )
}

export default Table

CSS側(CSS Modules使ってます)

.table {
  display: block;
  border-spacing: 0;;
}

.thead_wrapper {
  border: 1px solid #888;
  border-bottom: none;
  background: #efefef;
}
.thead {
  overflow-x: hidden;
}

.tbody {
  border: 1px solid #888;
  border-top: none;
}

.th {
  font-weight: bold;
}
.th, .td {
  margin: 0;
  padding: 0.5rem;
  border-bottom: 1px solid #888;
  border-right: 1px solid #888;
}

App コンポーネントの実装

選択されたCSVファイルを読み込み、データを上で作成した Table に渡す App コンポーネントを実装します。
1行目のデータをヘッダとして読み込むか、明細データとして読み込むかのオプションを指定するチェックボックスも付けます。

以下が App コンポーネントの実装例です。

import Papa from 'papaparse';
import { ChangeEvent, useMemo, useState } from 'react';
import Table from '../../components/atom/Table/Table';


function App() {

  //CSVから読み込んだデータ
  const [csvData, setCsvData] = useState<Papa.ParseResult<unknown> | null>(null)

  //CSVの1行目をヘッダ行とするか
  const [headerFirst, setHeaderFirst] = useState<boolean>(false)

  //ファイルを選択
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files as FileList
    if (files.length == 0) return

    //CSVをパース
    Papa.parse(files[0], {
      complete: function(results) {
        // パースが完了したら、結果を表示する
        console.log(results);
        setCsvData(results)
      }
    })
  };

  //テーブルに表示する列の定義(CSVの1行目から作成)
  const columns = useMemo(() => {
    if (csvData == null || csvData.data.length == 0) {
      return [ { Header: 'No Data' } ]
    }
    //1行目のデータで列の定義を作成
    const row = csvData.data[0] as Array<any>
    return row.map((cellData, columnIndex) => {
      return {
        Header: headerFirst ? cellData : `Column${columnIndex+1}`,
        accessor: (row: any, i: number) => row[columnIndex],      
        width: 160
      }
    })
  
  }, [csvData, headerFirst])

  return (
    <div>
      <div>
        <input type="file" onChange={handleChange}/>
        <label>
          <input type="checkbox" 
            onChange={(e) => setHeaderFirst(e.target.checked)}
            checked={headerFirst} />
          1行目をヘッダとして読み込む
        </label>
      </div>

      <Table 
        columns={columns} 
        data={csvData?.data.slice(headerFirst ? 1 : 0) ?? []}
        width={660} />
    </div>
  )
}

export default App

試してみる

作成したコードを実行して、実際に10万件のCSVデータをテーブルに表示させてみた。
結果、1秒程度でこんな感じで一覧に表示されました。

10万件のCSVを表示させた結果

スポンサーリンク
スポンサーリンク

このブログを検索

Profile

自分の写真
Webアプリエンジニア。 日々新しい技術を追い求めてブログでアウトプットしています。
プロフィール画像は、猫村ゆゆこ様に書いてもらいました。

仕事募集もしていたり、していなかったり。

QooQ