
MUI (Material UI)のTableは大量のデータだと重くなる
大量のデータを表示するReactアプリケーションでは、表示パフォーマンスの最適化が欠かせません。
特にMUI (Material UI)のTableを使用して大量のデータを読み込んでいる場合は、スクロールやクリックなどの処理が重くなってしまいます。

例えば、以下のテーブルは2000件のランダムなダミーデータを入れて、チェックボックスで選択して送信ボタンを押すとConsoleに選択したものを表示するという簡単なサンプルです。
このサンプルのチェックボックスを押すと、実際に押してから反映されるまで少し時間がかかることが確認できます。
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Checkbox from '@mui/material/Checkbox';
import Button from '@mui/material/Button';
import { useState } from 'react';
import Chance from 'chance';
interface Data {
id: number;
checkbox?: boolean;
name: string;
age: number;
phone: string;
state: string;
}
interface ColumnData {
dataKey: keyof Data;
label: string;
numeric?: boolean;
width?: number;
}
const chance = new Chance(1000);
function createData(id: number): Data {
return {
id,
name: chance.name(),
age: chance.age(),
state: chance.state({ full: true }),
phone: chance.phone(),
};
}
const columns: ColumnData[] = [
{
width: 50,
label: '',
dataKey: 'checkbox' as keyof Data,
},
{
width: 100,
label: 'Name',
dataKey: 'name',
},
{
width: 50,
label: 'Age',
dataKey: 'age',
numeric: true,
},
{
width: 100,
label: 'State',
dataKey: 'state',
},
{
width: 100,
label: 'Phone Number',
dataKey: 'phone',
},
];
const rows: Data[] = Array.from({ length: 2000 }, (_, index) =>
createData(index)
);
export default function ReactVirtualizedTable() {
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const handleRowSelect = (rowId: number) => {
const newSelected = new Set(selectedRows);
if (newSelected.has(rowId)) {
newSelected.delete(rowId);
} else {
newSelected.add(rowId);
}
setSelectedRows(newSelected);
};
const handleSelectAll = () => {
if (selectedRows.size === rows.length) {
setSelectedRows(new Set());
} else {
setSelectedRows(new Set(rows.map((row) => row.id)));
}
};
const handleSubmit = () => {
const selectedData = rows.filter((row) => selectedRows.has(row.id));
console.log('Selected rows:', selectedData);
};
return (
<>
<Paper
sx={{
height: 600,
width: '100%',
maxWidth: '800px',
marginInline: 'auto',
}}
>
<TableContainer sx={{ height: '100%', overflow: 'auto' }}>
<Table
sx={{
borderCollapse: 'separate',
tableLayout: 'fixed',
}}
>
<TableHead
sx={{
position: 'sticky',
top: 0,
zIndex: 2,
}}
>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.dataKey}
variant="head"
style={{ width: column.width }}
sx={{ backgroundColor: 'background.paper' }}
>
{column.dataKey === 'checkbox' ? (
<Checkbox
checked={selectedRows.size === rows.length}
indeterminate={
selectedRows.size > 0 &&
selectedRows.size < rows.length
}
onChange={handleSelectAll}
/>
) : (
column.label
)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id}>
{columns.map((column) => (
<TableCell key={column.dataKey}>
{column.dataKey === 'checkbox' ? (
<Checkbox
checked={selectedRows.has(row.id)}
onChange={() => handleRowSelect(row.id)}
/>
) : (
row[column.dataKey]
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
<div style={{ padding: '16px', textAlign: 'center' }}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={selectedRows.size === 0}
>
送信 ({selectedRows.size}件選択)
</Button>
</div>
</>
);
}
このようなケースでは、react-virtuosoをインストールして仮想レンダリングでスクロール位置に応じて必要な行だけ描画すれば問題を解決できます。
react-virtuosoの使い方
まず、以下のコマンドでreact-virtuosoをインストールします。
npm i react-virtuoso
react-virtuosoをインストールしたら、TableVirtuosoをインポートして、各プロパティで必要なものを指定します。
import { TableVirtuoso } from 'react-virtuoso'
<Paper style={{ height: 600, width: '100%' }}>
<TableVirtuoso
data={rows}
components={VirtuosoTableComponents}
fixedHeaderContent={fixedHeaderContent}
itemContent={rowContent}
/>
</Paper>
data={rows}
表示するデータ配列(=1行分のデータ)を指定します。
この記事の例だとcreateDataで生成したダミーデータ配列を使用しています。
const rows: Data[] = Array.from({ length: 2000 }, (_, index) =>
createData(index)
)
components={VirtuosoTableComponents}
TableVirtuoso に対して、thead, tbody, tr, td などをどのように描画するかを指定します。
この記事の例だとMUIを使用しているので以下のように各要素を指定しています。
import type { TableComponents } from 'react-virtuoso'
const VirtuosoTableComponents: TableComponents<Data> = {
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
<TableContainer component={Paper} {...props} ref={ref} />
)),
Table: (props) => (
<Table {...props} sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }} />
),
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableHead {...props} ref={ref} />
)),
TableRow,
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableBody {...props} ref={ref} />
)),
}
fixedHeaderContent={fixedHeaderContent}
テーブルヘッダーの中身を定義する関数です。
このテーブルヘッダーは「position: 'sticky'」を指定しなくても固定になります。
const fixedHeaderContent = () => {
return (
<TableRow>
{columns.map((column) => (
<TableCell
key={column.dataKey}
variant="head"
sx={{ width: column.width, backgroundColor: 'background.paper' }}
>
{column.dataKey === 'checkbox' ? (
<Checkbox
checked={selectedRows.size === rows.length}
indeterminate={
selectedRows.size > 0 &&
selectedRows.size < rows.length
}
onChange={handleSelectAll}
/>
) : (
column.label
)}
</TableCell>
))}
</TableRow>
)
}
itemContent={(index, item) => JSX}
テーブルの各行のセルをレンダリングする関数です。
返り値としてテーブルのセル <TableCell> を複数返します。
const rowContent = (_index: number, row: Data) => {
return columns.map((column) => (
<TableCell key={column.dataKey}>
{column.dataKey === 'checkbox' ? (
<Checkbox
checked={selectedRows.has(row.id)}
onChange={() => handleRowSelect(row.id)}
/>
) : (
row[column.dataKey]
)}
</TableCell>
))
}
以上の点を踏まえた上で作成したものが、以下のコードになります。
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import Paper from '@mui/material/Paper'
import Checkbox from '@mui/material/Checkbox'
import Button from '@mui/material/Button'
import { TableVirtuoso } from 'react-virtuoso'
import type { TableComponents } from 'react-virtuoso'
import React, { useState } from 'react'
import Chance from 'chance'
interface Data {
id: number
checkbox?: boolean
name: string
age: number
phone: string
state: string
}
interface ColumnData {
dataKey: keyof Data
label: string
numeric?: boolean
width?: number
}
const chance = new Chance(1000)
function createData(id: number): Data {
return {
id,
name: chance.name(),
age: chance.age(),
state: chance.state({ full: true }),
phone: chance.phone(),
}
}
const columns: ColumnData[] = [
{
width: 50,
label: '',
dataKey: 'checkbox' as keyof Data,
},
{
width: 100,
label: 'Name',
dataKey: 'name',
},
{
width: 50,
label: 'Age',
dataKey: 'age',
numeric: true,
},
{
width: 100,
label: 'State',
dataKey: 'state',
},
{
width: 100,
label: 'Phone Number',
dataKey: 'phone',
},
]
const rows: Data[] = Array.from({ length: 2000 }, (_, index) =>
createData(index)
)
const VirtuosoTableComponents: TableComponents<Data> = {
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
<TableContainer component={Paper} {...props} ref={ref} />
)),
Table: (props) => (
<Table {...props} sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }} />
),
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableHead {...props} ref={ref} />
)),
TableRow,
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableBody {...props} ref={ref} />
)),
}
export default function ReactVirtualizedTable() {
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set())
const handleRowSelect = (rowId: number) => {
const newSelected = new Set(selectedRows)
if (newSelected.has(rowId)) {
newSelected.delete(rowId)
} else {
newSelected.add(rowId)
}
setSelectedRows(newSelected)
}
const handleSelectAll = () => {
if (selectedRows.size === rows.length) {
setSelectedRows(new Set())
} else {
setSelectedRows(new Set(rows.map((row) => row.id)))
}
}
const handleSubmit = () => {
const selectedData = rows.filter((row) => selectedRows.has(row.id))
console.log('Selected rows:', selectedData)
}
const fixedHeaderContent = () => {
return (
<TableRow>
{columns.map((column) => (
<TableCell
key={column.dataKey}
variant="head"
sx={{ width: column.width, backgroundColor: 'background.paper' }}
>
{column.dataKey === 'checkbox' ? (
<Checkbox
checked={selectedRows.size === rows.length}
indeterminate={
selectedRows.size > 0 &&
selectedRows.size < rows.length
}
onChange={handleSelectAll}
/>
) : (
column.label
)}
</TableCell>
))}
</TableRow>
)
}
const rowContent = (_index: number, row: Data) => {
return columns.map((column) => (
<TableCell key={column.dataKey}>
{column.dataKey === 'checkbox' ? (
<Checkbox
checked={selectedRows.has(row.id)}
onChange={() => handleRowSelect(row.id)}
/>
) : (
row[column.dataKey]
)}
</TableCell>
))
}
return (
<>
<Paper style={{ height: 600, width: '100%' }}>
<TableVirtuoso
data={rows}
components={VirtuosoTableComponents}
fixedHeaderContent={fixedHeaderContent}
itemContent={rowContent}
/>
</Paper>
<div style={{ padding: '16px', textAlign: 'center' }}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
disabled={selectedRows.size === 0}
>
送信 ({selectedRows.size}件選択)
</Button>
</div>
</>
)
}
作成したサンプルを見るとチェックボックスをクリックしても、すぐに選択が反映されていることが確認できます。
データは2000件にしていますが、react-virtuosoは仮想化技術を使って表示されている範囲だけDOMに描画しているため、この描画方法であればデータが数万件あったとしても高速でスクロールやクリックなどができます。
MUIのテーブルにreact-virtuosoを使用したサンプル