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秒程度でこんな感じで一覧に表示されました。
0 件のコメント:
コメントを投稿