Material-UI Table With Infinite Scroll
May 30, 2021

At the time of writing this post, the version of @material-ui/core
is 4.11.4
.
Currently, the Table component doesn't come with an infinite scroll out of the box. The XGrid component supports infinite scroll, but in some cases, you may need or prefer to use the Table component instead.
For those cases, here's a solution to help implement infinite scroll.
Let's start by creating a table with some dummy data.
import React, { useState } from 'react'
import Table from '@material-ui/core/Table'
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
const generateItems = amount => {
const arr = Array.from(Array(amount))
return arr.map((number, i) => ({
id: i,
name: `Name ${i + 1}`,
type: `Item Type ${i + 1}`,
}))
}
const TableWithInfiniteScroll = () => {
const [rows, setRows] = useState(generateItems(50))
return (
<TableContainer style={{ maxWidth: '600px', margin: 'auto', maxHeight: '300px' }}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Type</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(({ id, name, type }) => (
<TableRow key={id}>
<TableCell>{name}</TableCell>
<TableCell>{type}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
export default TableWithInfiniteScroll
Next, we'll add the scroll listener.
...
const tableEl = useRef()
const scrollListener = useCallback(() => {
//
}, [])
useLayoutEffect(() => {
const tableRef = tableEl.current
tableRef.addEventListener('scroll', scrollListener)
return () => {
tableRef.removeEventListener('scroll', scrollListener)
}
}, [scrollListener])
return (
<TableContainer style={{ maxWidth: '600px', margin: 'auto', maxHeight: '300px' }} ref={tableEl}>
...
We'll use this function to simulate data fetching.
...
const loadMore = useCallback(() => {
const loadItems = async () => {
await new Promise(resolve =>
setTimeout(() => {
const amount = rows.length + 50
setRows(generateItems(amount))
setLoading(false)
resolve()
}, 2000)
)
}
setLoading(true)
loadItems()
}, [rows])
...
Now, we need to add the correct logic to the scrollListener
function to trigger the API call (in this case, the loadMore
function) at the right moment — when the user scrolls near the bottom of the list.
...
const [distanceBottom, setDistanceBottom] = useState(0)
// hasMore should come from the place where you do the data fetching
// for example, it could be a prop passed from the parent component
// or come from some store
const [hasMore] = useState(true)
...
const scrollListener = useCallback(() => {
let bottom = tableEl.current.scrollHeight - tableEl.current.clientHeight
// if you want to change distanceBottom every time new data is loaded
// don't use the if statement
if (!distanceBottom) {
// calculate distanceBottom that works for you
setDistanceBottom(Math.round(bottom * 0.2))
}
if (tableEl.current.scrollTop > bottom - distanceBottom && hasMore && !loading) {
loadMore()
}
}, [hasMore, loadMore, loading, distanceBottom])
...
Full Example
import React, { useState, useRef, useLayoutEffect, useCallback } from 'react'
import Table from '@material-ui/core/Table'
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
import CircularProgress from '@material-ui/core/CircularProgress'
const generateItems = amount => {
const arr = Array.from(Array(amount))
return arr.map((number, i) => ({
id: i,
name: `Name ${i + 1}`,
type: `Item Type ${i + 1}`,
}))
}
const TableWithInfiniteScroll = () => {
const tableEl = useRef()
const [rows, setRows] = useState(generateItems(50))
const [loading, setLoading] = useState(false)
const [distanceBottom, setDistanceBottom] = useState(0)
// hasMore should come from the place where you do the data fetching
// for example, it could be a prop passed from the parent component
// or come from some store
const [hasMore] = useState(true)
const loadMore = useCallback(() => {
const loadItems = async () => {
await new Promise(resolve =>
setTimeout(() => {
const amount = rows.length + 50
setRows(generateItems(amount))
setLoading(false)
resolve()
}, 2000),
)
}
setLoading(true)
loadItems()
}, [rows])
const scrollListener = useCallback(() => {
let bottom = tableEl.current.scrollHeight - tableEl.current.clientHeight
// if you want to change distanceBottom every time new data is loaded
// don't use the if statement
if (!distanceBottom) {
// calculate distanceBottom that works for you
setDistanceBottom(Math.round(bottom * 0.2))
}
if (tableEl.current.scrollTop > bottom - distanceBottom && hasMore && !loading) {
loadMore()
}
}, [hasMore, loadMore, loading, distanceBottom])
useLayoutEffect(() => {
const tableRef = tableEl.current
tableRef.addEventListener('scroll', scrollListener)
return () => {
tableRef.removeEventListener('scroll', scrollListener)
}
}, [scrollListener])
return (
<TableContainer style={{ maxWidth: '600px', margin: 'auto', maxHeight: '300px' }} ref={tableEl}>
{loading && <CircularProgress style={{ position: 'absolute', top: '100px' }} />}
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Type</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(({ id, name, type }) => (
<TableRow key={id}>
<TableCell>{name}</TableCell>
<TableCell>{type}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
export default TableWithInfiniteScroll