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 has infinite scroll, but in some cases, you might need to stick to the Table component, or you just want to use the Table component.
For those cases, here is a solution that could help you with the infinite scroll implementation.
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
Now we can start adding the main parts. 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 just need to add a proper logic to the scrollListener
function that would trigger the API call at the right time (in this case, loadMore
function) - when the user scrolls almost to 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])
...
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