How to Build a Sortable and Paginated Table in React MUI

Displaying data is one thing, but making it useful for your users is an entirely different challenge.

In my eight years of building React applications, I’ve found that users almost always demand three things: clarity, speed, and control.

When you’re dealing with hundreds of rows, like a list of real estate listings in New York or employee payroll data, a flat list just doesn’t cut it.

That is where Material UI (MUI) comes in. It provides a robust set of components that handle the heavy lifting of UI design so you can focus on logic.

In this tutorial, I will show you how to build a professional-grade table that supports both sorting and pagination from scratch.

Why Use MUI for Data Tables?

MUI is the industry standard for React apps in the US. It follows Material Design principles, ensuring your app looks polished and trustworthy.

The Table component in MUI is highly modular. You aren’t stuck with a “black box” widget; you have full control over the rows, cells, and headers.

Set Up Your React Environment

Before we dive into the code, ensure you have a React project ready and the necessary MUI packages installed.

You can install them using npm:

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

For our examples, I’ll use a dataset representing Tech Companies in the United States, including their headquarters and employee counts.

Method 1: Build a Client-Side Sortable and Paginated Table

This is the most common approach when you have a moderate amount of data (up to a few thousand rows) that you can load into the browser at once.

I prefer this method for internal dashboards because it feels incredibly snappy for the end-user.

The Full Implementation Code

Here is the complete code for a functional, sortable table.

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

// Sample US-based Data
const companyData = [
  { id: 1, name: 'Apple', sector: 'Technology', location: 'Cupertino, CA', employees: 164000 },
  { id: 2, name: 'Microsoft', sector: 'Technology', location: 'Redmond, WA', employees: 221000 },
  { id: 3, name: 'Amazon', sector: 'E-commerce', location: 'Seattle, WA', employees: 1541000 },
  { id: 4, name: 'Alphabet', sector: 'Technology', location: 'Mountain View, CA', employees: 190000 },
  { id: 5, name: 'Tesla', sector: 'Automotive', location: 'Austin, TX', employees: 127000 },
  { id: 6, name: 'Meta', sector: 'Social Media', location: 'Menlo Park, CA', employees: 86000 },
  { id: 7, name: 'Netflix', sector: 'Entertainment', location: 'Los Gatos, CA', employees: 12000 },
  { id: 8, name: 'Salesforce', sector: 'Software', location: 'San Francisco, CA', employees: 79000 },
  { id: 9, name: 'Adobe', sector: 'Software', location: 'San Jose, CA', employees: 26000 },
  { id: 10, name: 'Intel', sector: 'Semiconductors', location: 'Santa Clara, CA', employees: 121000 },
];

export default function USCompanyTable() {
  const [order, setOrder] = useState('asc');
  const [orderBy, setOrderBy] = useState('employees');
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);

  // Sorting Logic
  const handleRequestSort = (property) => {
    const isAsc = orderBy === property && order === 'asc';
    setOrder(isAsc ? 'desc' : 'asc');
    setOrderBy(property);
  };

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

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

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

  const sortedData = companyData.sort(getComparator(order, orderBy));
  const paginatedData = sortedData.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);

  return (
    <Box sx={{ width: '100%', p: 3 }}>
      <Typography variant="h4" gutterBottom>
        Top US Tech Companies
      </Typography>
      <TableContainer component={Paper} elevation={3}>
        <Table>
          <TableHead sx={{ backgroundColor: '#f5f5f5' }}>
            <TableRow>
              <TableCell>
                <TableSortLabel
                  active={orderBy === 'name'}
                  direction={orderBy === 'name' ? order : 'asc'}
                  onClick={() => handleRequestSort('name')}
                >
                  Company Name
                </TableSortLabel>
              </TableCell>
              <TableCell>Sector</TableCell>
              <TableCell>Headquarters</TableCell>
              <TableCell align="right">
                <TableSortLabel
                  active={orderBy === 'employees'}
                  direction={orderBy === 'employees' ? order : 'asc'}
                  onClick={() => handleRequestSort('employees')}
                >
                  Employees
                </TableSortLabel>
              </TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {paginatedData.map((row) => (
              <TableRow key={row.id} hover>
                <TableCell style={{ fontWeight: 'bold' }}>{row.name}</TableCell>
                <TableCell>{row.sector}</TableCell>
                <TableCell>{row.location}</TableCell>
                <TableCell align="right">{row.employees.toLocaleString()}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
        <TablePagination
          rowsPerPageOptions={[5, 10, 25]}
          component="div"
          count={companyData.length}
          rowsPerPage={rowsPerPage}
          page={page}
          onPageChange={handleChangePage}
          onRowsPerPageChange={handleChangeRowsPerPage}
        />
      </TableContainer>
    </Box>
  );
}

You can refer to the screenshot below to see the output.

Sortable and Paginated Table in React MUI

The TableSortLabel is the key component here. It creates that clickable header with an arrow icon.

I used a comparator function to handle the sorting logic. This is a standard JavaScript pattern that works perfectly with React state.

The pagination is handled by slicing the array. We calculate the start and end indices using the page and rowsPerPage state variables.

Method 2: Customize the Table for Better UX

Sometimes a standard table isn’t enough. In my experience, users in high-stakes environments, like financial services in Charlotte or Chicago, need visual cues.

You can add conditional styling or “Empty State” handling to make the table more professional.

Example: Add Search and Highlighted Rows

Adding a search filter is the most requested feature after sorting. Here is how you can integrate it seamlessly.

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

const stateData = [
  { id: 1, state: 'Texas', capital: 'Austin', population: 29145505 },
  { id: 2, state: 'California', capital: 'Sacramento', population: 39538223 },
  { id: 3, state: 'Florida', capital: 'Tallahassee', population: 21538187 },
  { id: 4, state: 'New York', capital: 'Albany', population: 20201249 },
];

export default function SearchableTable() {
  const [searchTerm, setSearchTerm] = useState('');

  const filteredStates = stateData.filter(item => 
    item.state.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <Box sx={{ p: 4 }}>
      <TextField 
        label="Search US States" 
        variant="outlined" 
        fullWidth 
        margin="normal"
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <TableContainer component={Paper}>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>State</TableCell>
              <TableCell>Capital</TableCell>
              <TableCell align="right">Population</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {filteredStates.map((row) => (
              <TableRow key={row.id} sx={{ '&:nth-of-type(odd)': { backgroundColor: '#fafafa' } }}>
                <TableCell>{row.state}</TableCell>
                <TableCell>{row.capital}</TableCell>
                <TableCell align="right">{row.population.toLocaleString()}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </Box>
  );
}

You can refer to the screenshot below to see the output.

Build Sortable and Paginated Table in React MUI

Always remember to use .toLowerCase() on both the search term and the data.

Users expect a case-insensitive search. If they type “texas,” they expect to see “Texas” in the results.

Important Considerations for Pagination

When building tables for US clients, you should consider whether you want Client-side or Server-side pagination.

Client-side pagination is what we built above. It is fast because the data is already in memory.

Server-side pagination is necessary if you have 100,000+ rows. In that case, you only fetch 10 or 20 rows at a time from the API.

If you choose server-side, you will update the page state and then trigger a useEffect hook to fetch new data from your backend.

Common Mistakes to Avoid

In my years of code reviews, I see developers making these mistakes often:

  1. Not using unique keys: Always use a unique ID for the key prop in TableRow. Never use the array index if your data can be sorted.
  2. Ignoring accessibility: MUI handles a lot of this, but ensure your TableSortLabel has clear text for screen readers.
  3. Hardcoding column widths: Let the table handle widths automatically unless you have a specific reason to fix a column (like an Actions menu).

Building a sortable and paginated table in React MUI is a skill you will use in almost every professional project.

It provides a clean, structured way for users to interact with large datasets without feeling overwhelmed.

You may 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.