I try to use a virtualized table with MUI table and TableVirtuoso
described in their docs and to enable sorting like here. In that sorting example the actual sorting is realized while rendering the rows from the data array, but the TableVirtuoso
component takes the complete data array and requires a render function for each row (parameter itemContent
), that returns a single row.
My idea was to realize the sorting on the data array, before given to the TableVirtuoso
component, when data, sort direction or sorted column changes using states and the useEffect
hook. Using my code in the example below, the data in the array is sorted, but the changes were not shown in the UI until you manually scroll the table. Even if the table is scrolled down before sorting is done, the programmatical scroll-to-top doesn't change the UI.
For me it feels like changing the data array doesn't cause a re-render of the table inside the virtuoso components as I expect. Is there a way to trigger the re-render on sorting, or which way would solve sorting in my case?
'use strict';
import React, {
forwardRef,
useState,
useEffect,
useRef,
} from 'react';
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 TableSortLabel from '@mui/material/TableSortLabel';
import Paper from '@mui/material/Paper';
import { TableVirtuoso } from 'react-virtuoso';
import { useTranslation } from './contexts/TranslationContext';
export const VirtualTable = (props) => {
// get props
const {
columnConfig,
data: propData
} = props;
// get translation hook
const { t, locale } = useTranslation();
// get virtuoso ref
const virtuoso = useRef(null);
// get data state
const [data, setData] = useState(propData);
// get order state
const [order, setOrder] = useState('asc');
// get order by state
const [orderBy, setOrderBy] = useState(columnConfig[0].dataKey);
// handle change of propData, order and orderBy and sort data
useEffect(() => {
setData((oldData) => oldData.sort(getComparator(order, orderBy)));
}, [propData, order, orderBy]);
// prepare sort comparators
const descendingComparator = (a, b, orderBy) => {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
};
const getComparator = (order, orderBy) => {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
};
// prepare Virtuoso components
const virtuosoComponents = {
Scroller: forwardRef((props, ref) => (
<TableContainer component={Paper} {...props} ref={ref} />
)),
Table: (props) => <Table {...props} sx={{ borderCollapse: 'separate' }} />,
TableHead: forwardRef((props, ref) => <TableHead {...props} sx={{ boxShadow: 3 }} ref={ref} />),
TableRow: ({ item: _, ...props }) => <TableRow {...props} hover={true} />,
TableBody: forwardRef((props, ref) => <TableBody {...props} ref={ref} />),
};
// event handler to handle onClick for sorting table
const handleSort = (columnDataKey) => {
const isAsc = orderBy === columnDataKey && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(columnDataKey);
virtuoso.current.scrollToIndex(0);
};
// format cell data according to type
const getCellData = (row, column) => {
// switch type
switch(column.type) {
case 'string':
return row[column.dataKey];
case 'date':
if(row[column.dataKey] === '') {
return '';
}
return (new Date(row[column.dataKey])).toLocaleDateString(locale, {day: '2-digit', month: '2-digit', year: 'numeric'});
case 'actions':
return '';
default:
return t('Column type "{{type}}" is not valid', {type: column.type});
}
};
// prepare fixed header
const getFixedHeader = () => {
return (
<TableRow>
{
columnConfig.map((column) => (
<TableCell
key={column.dataKey}
align={column.align}
sx={{
width: typeof column.width !== 'undefined' ? column.width : undefined,
backgroundColor: 'background.paper',
}}
>
{typeof column.sortable !== 'undefined' && column.sortable === false
? column.label
: <TableSortLabel
active={orderBy === column.dataKey}
direction={orderBy === column.dataKey ? order : 'asc'}
onClick={() => handleSort(column.dataKey)}
>
{column.label}
</TableSortLabel>
}
</TableCell>
))
}
</TableRow>
);
};
// prepare row content
const getRowContent = (_index, row) => {
return (
<>
{columnConfig.map((column) => (
<TableCell
key={column.dataKey}
align={column.align}
>
{getCellData(row, column)}
</TableCell>
))}
</>
);
};
// render jsx
return (
<Paper style={{ width: '100%', height: 'calc(100vh - 160px' }} >
<TableVirtuoso
ref={virtuoso}
data={data}
fixedHeaderContent={getFixedHeader}
itemContent={getRowContent}
components={virtuosoComponents}
overscan={{ main: 5, reverse: 5 }}
/>
</Paper>
);
};
const columnConfig = [
{
label: t('Begin'),
dataKey: 'begin',
align: 'left',
width: '10%',
type: 'date',
},
{
label: t('End'),
dataKey: 'end',
align: 'left',
width: '10%',
type: 'date',
sortable: false,
},
{
label: t('Event'),
dataKey: 'name',
align: 'left',
type: 'string',
},
{
label: t('Location'),
dataKey: 'location',
align: 'left',
width: '15%',
type: 'string',
},
{
label: t('Actions'),
dataKey: 'actions',
align: 'right',
width: '15%',
type: 'actions',
sortable: false,
actions: [
],
},
];
const data = Array.from({ length: 100 }, (_, index) => {
const now = new Date();
const begin = (new Date()).setDate(now.getDate() + index);
return {
id: index,
begin: begin,
end: '',
name: `Event ${index}`,
location: `Location ${index}`,
actions: '',
};
});
During further development to realize instant search in the table I had to alter the data array more and that lead me to some explanation and a solution.
Array.prototype.sort()
does its job altering the source array:
The sort() method sorts the elements of an array in place and returns the reference to the same array, now sorted.
and resetting the state like I did, isn't recognized by the TabelVirtuoso
component. So the solution is simply make a copy of the data array, sort it and set the copy as new state.
useEffect(() => {
setData([...data].sort(getComparator(order, orderBy));
}, [propData, order, orderBy]);