How to Implement TablePagination in React MUI Tables

When I first started building data-heavy dashboards in React, I quickly realized that dumping thousands of rows into a single table was a recipe for a sluggish UI.

Over my eight years of developing React applications, I’ve found that Material UI (MUI) provides one of the most robust sets of components for handling large datasets efficiently.

Pagination is not just about performance; it is about providing a clean, professional experience for your users.

In this tutorial, I will walk you through exactly how I implement TablePagination in React MUI tables, using real-world examples you would see in a professional US-based business environment.

Why Use MUI TablePagination?

The TablePagination component is one of the most versatile pieces in the MUI library.

It handles the complex logic of “Rows per page,” navigation buttons, and labels with very little boilerplate code.

Set Up the Project

Before we dive into the methods, ensure you have the necessary MUI packages installed in your React project.

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

For our examples, we will use a dataset representing US Real Estate Listings, which is a common use case for professional data tables.

Method 1: Client-Side Pagination for Smaller Datasets

I use this method when the entire dataset is small enough to be loaded into the browser memory at once.

It is incredibly fast for the user because there are no additional network requests when they click “Next.”

The Implementation

In this example, we manage the state for the current page and the number of rows per page using React hooks.

import React, { useState } from 'react';
import {
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Paper,
  TablePagination,
  Typography
} from '@mui/material';

// Sample US Real Estate Data
const US_REAL_ESTATE_DATA = [
  { id: 1, address: '123 Maple St, Austin, TX', price: '$450,000', sqft: 2100 },
  { id: 2, address: '456 Oak Ave, Denver, CO', price: '$525,000', sqft: 1850 },
  { id: 3, address: '789 Pine Ln, Seattle, WA', price: '$890,000', sqft: 2400 },
  { id: 4, address: '101 Cedar Rd, Miami, FL', price: '$310,000', sqft: 1500 },
  { id: 5, address: '202 Birch Dr, Phoenix, AZ', price: '$415,000', sqft: 1950 },
  { id: 6, address: '303 Spruce Ct, Nashville, TN', price: '$480,000', sqft: 2200 },
  { id: 7, address: '404 Elm St, Chicago, IL', price: '$395,000', sqft: 1700 },
  { id: 8, address: '505 Willow Way, Atlanta, GA', price: '$435,000', sqft: 2050 },
  { id: 9, address: '606 Ash Blvd, Boston, MA', price: '$720,000', sqft: 1600 },
  { id: 10, address: '707 Walnut Dr, San Jose, CA', price: '$1,200,000', sqft: 2500 },
  { id: 11, address: '808 Redwood Dr, Portland, OR', price: '$560,000', sqft: 2100 },
];

export default function ClientSidePagination() {
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };

  // Logic to slice the data based on current page
  const visibleRows = US_REAL_ESTATE_DATA.slice(
    page * rowsPerPage,
    page * rowsPerPage + rowsPerPage
  );

  return (
    <Paper sx={{ width: '100%', overflow: 'hidden', padding: 2 }}>
      <Typography variant="h6" component="div" sx={{ mb: 2 }}>
        US Real Estate Market Listings
      </Typography>
      <TableContainer sx={{ maxHeight: 440 }}>
        <Table stickyHeader aria-label="real estate table">
          <TableHead>
            <TableRow>
              <TableCell sx={{ fontWeight: 'bold' }}>Property ID</TableCell>
              <TableCell sx={{ fontWeight: 'bold' }}>Address</TableCell>
              <TableCell sx={{ fontWeight: 'bold' }} align="right">Price (USD)</TableCell>
              <TableCell sx={{ fontWeight: 'bold' }} align="right">Square Feet</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {visibleRows.map((row) => (
              <TableRow hover key={row.id}>
                <TableCell>{row.id}</TableCell>
                <TableCell>{row.address}</TableCell>
                <TableCell align="right">{row.price}</TableCell>
                <TableCell align="right">{row.sqft}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
      <TablePagination
        rowsPerPageOptions={[5, 10, 25]}
        component="div"
        count={US_REAL_ESTATE_DATA.length}
        rowsPerPage={rowsPerPage}
        page={page}
        onPageChange={handleChangePage}
        onRowsPerPageChange={handleChangeRowsPerPage}
      />
    </Paper>
  );
}

You can see the output in the screenshot below.

TablePagination in React MUI Tables

The page state tracks the zero-indexed page number. The rowsPerPage state controls how many items are shown at once.

We use the .slice() method to create a subset of the array to display.

Method 2: Server-Side Pagination for Enterprise Apps

In most professional US-based enterprise applications, you’ll be dealing with millions of records. Fetching all that data at once would crash the user’s browser.

In this scenario, we only fetch the data for the specific page the user is viewing.

The Implementation

For this example, imagine we are managing Corporate Employee Records for a company like a US-based tech firm.

import React, { useState, useEffect } from 'react';
import {
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Paper,
  TablePagination,
  CircularProgress,
  Box
} from '@mui/material';

export default function ServerSidePagination() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [totalCount, setTotalCount] = useState(0);
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(10);

  // Simulated API Call to fetch US Employee Data
  const fetchEmployees = async (currentPage, limit) => {
    setLoading(true);
    // In a real app, this would be: 
    // fetch(`https://api.acme-corp.us/employees?page=${currentPage}&limit=${limit}`)
    
    setTimeout(() => {
      // Mocking a response with 100 total employees
      const mockData = Array.from({ length: limit }, (_, i) => ({
        id: currentPage * limit + i + 1,
        name: `Employee ${currentPage * limit + i + 1}`,
        department: i % 2 === 0 ? 'Engineering' : 'Marketing',
        location: 'San Francisco, CA',
        salary: `$${(Math.random() * 50000 + 100000).toFixed(0)}`
      }));
      
      setData(mockData);
      setTotalCount(100); // Assume 100 total records in DB
      setLoading(false);
    }, 800);
  };

  useEffect(() => {
    fetchEmployees(page, rowsPerPage);
  }, [page, rowsPerPage]);

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };

  return (
    <Paper sx={{ width: '100%' }}>
      <TableContainer>
        {loading ? (
          <Box sx={{ display: 'flex', justifyContent: 'center', p: 5 }}>
            <CircularProgress />
          </Box>
        ) : (
          <Table>
            <TableHead>
              <TableRow>
                <TableCell>Emp ID</TableCell>
                <TableCell>Name</TableCell>
                <TableCell>Department</TableCell>
                <TableCell>Office Location</TableCell>
                <TableCell align="right">Annual Salary</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {data.map((row) => (
                <TableRow key={row.id}>
                  <TableCell>{row.id}</TableCell>
                  <TableCell>{row.name}</TableCell>
                  <TableCell>{row.department}</TableCell>
                  <TableCell>{row.location}</TableCell>
                  <TableCell align="right">{row.salary}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </TableContainer>
      <TablePagination
        rowsPerPageOptions={[10, 25, 50]}
        component="div"
        count={totalCount}
        rowsPerPage={rowsPerPage}
        page={page}
        onPageChange={handleChangePage}
        onRowsPerPageChange={handleChangeRowsPerPage}
      />
    </Paper>
  );
}

You can see the output in the screenshot below.

Implement TablePagination in React MUI Tables

When we change the page, the useEffect hook triggers a new API call. This keeps the memory footprint low and the application responsive.

Customize the Pagination Labels

A common request I get from US clients is to change the “Rows per page:” text to something more specific, like “Listings per page.”

You can easily do this using the labelRowsPerPage prop.

<TablePagination
  labelRowsPerPage="Results per page:"
  labelDisplayedRows={({ from, to, count }) => `${from}-${to} of ${count} records`}
  // ... other props
/>

Common Challenges and My Tips

One thing I’ve learned is that when you change the rowsPerPage, you should always reset the page to 0.

If you are on page 10 and change the limit so that there are only 5 pages total, the table will appear empty unless you reset the index.

Also, always ensure your backend developer knows that MUI uses 0-based indexing for pages.

Some APIs use 1-based indexing, which can cause annoying “off-by-one” errors.

Summary of Components Used

ComponentPurpose
TablePaginationThe main controller for the navigation UI.
TableContainerWraps the table to provide scrolling behavior.
PaperProvides the professional “card” look with shadows.

Implementing pagination in MUI is easy once you understand the relationship between the state and the data slicing logic.

Whether you choose client-side or server-side depends entirely on the size of your dataset and the speed of your API.

I hope this guide helps you build better, faster React tables for your users!

In this tutorial, I’ve shown you two practical ways to handle pagination in React MUI tables. Using these methods will ensure your application remains performant as your data grows. I’ve personally found that the server-side approach is almost always better for long-term scalability.

You can also like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.