Material-UI Table With Infinite Scroll

May 30, 2021

material ui table with infinite scroll

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])

...

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

Result