How to solve sorting with MUITable in TableVirtuoso requires manual scroll inside table to actually happen?

1.2k Views Asked by At

Problem

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?

Code

Component:

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

Prop columnConfig:

    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: [
            
            ],
        },
    ];

Prop data:

    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: '',
        };
    });
1

There are 1 best solutions below

0
On BEST ANSWER

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.

MDN

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.

Solution

useEffect(() => {
    setData([...data].sort(getComparator(order, orderBy));
}, [propData, order, orderBy]);