mui-datatables React: Advanced Data Table Tutorial & Setup








mui-datatables in React: The Complete Advanced Data Table Guide

Updated 2025  ·  18 min read  · 
React 18 · MUI v5 · mui-datatables 4.x

React
Material UI
Data Table
mui-datatables
Enterprise
Server-Side

If you’ve ever tried building a production-grade data table in React from scratch,
you know the drill: sorting works until it doesn’t, pagination breaks on page three,
and custom cell rendering turns into a 400-line component nobody wants to touch again.
mui-datatables
exists precisely to save you from that fate. It’s a fully open-source
React Material-UI table component
that ships with sorting, filtering, pagination, search, column resizing, and
server-side data support — all baked in, all configurable, none of it paywalled.

This guide walks you through everything: installation, core configuration,
advanced rendering patterns, server-side integration, and enterprise-level
customization. Whether you’re wiring up a simple admin panel or a data-heavy
analytics dashboard, you’ll leave here with a component that actually works.


Why mui-datatables Still Wins in 2025

The React ecosystem is crowded with table libraries — TanStack Table, AG Grid,
react-virtualized, and the official MUI DataGrid all compete for the same real
estate. So why choose
mui-datatables?
The short answer: it hits the exact middle ground between „too simple to be useful”
and „too complex to configure without a PhD.”

MUI DataGrid — the official offering from the MUI team — is excellent, but its most
useful features (row grouping, Excel export, advanced filtering) are locked behind
the Pro and Premium tiers, which start at $180 per developer per year. For startups,
solo engineers, and open-source projects, that’s a real constraint.
mui-datatables
gives you most of those features — for free — with an API that takes an afternoon to learn
and a day to master.

The library also integrates seamlessly with MUI v5’s theming system. That means your
React Material-UI data table
will automatically inherit your application’s color palette, typography, and spacing —
no manual style overrides needed unless you want them. For design-system-driven teams,
this is a significant quality-of-life win.


Installation and Initial Setup

Getting
mui-datatables installed
is refreshingly straightforward. The library depends on MUI v5,
so if you’re already running a Material UI project, you’re halfway there.
If not, the following command installs everything you need in one shot:

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

Once installed, the entry point is the MUIDataTable component.
It accepts three primary props: title (a string or React node rendered
in the table toolbar), columns (an array of column definition objects),
and data (a two-dimensional array or array of objects representing your rows).
The fourth prop, options, is where all the interesting configuration lives.

Here’s the minimum viable
mui-datatables setup
that renders a working, sortable, searchable table in under 20 lines:

import MUIDataTable from "mui-datatables";

const columns = [
  { name: "id",    label: "ID"    },
  { name: "name",  label: "Name"  },
  { name: "role",  label: "Role"  },
  { name: "email", label: "Email" },
];

const data = [
  { id: 1, name: "Alice Chen",   role: "Engineer", email: "alice@corp.io"  },
  { id: 2, name: "Bob Martins",  role: "Designer", email: "bob@corp.io"    },
  { id: 3, name: "Carol White",  role: "Manager",  email: "carol@corp.io"  },
];

const options = {
  filterType: "checkbox",
  responsive: "vertical",
  selectableRows: "none",
};

export default function UsersTable() {
  return (
    <MUIDataTable
      title="User Directory"
      columns={columns}
      data={data}
      options={options}
    />
  );
}

That’s it. You now have a
React interactive table
with column sorting, a global search bar, column visibility toggles, and download
functionality — all from a single component declaration. From here, the options object
is your primary lever for controlling behavior.


Column Configuration: The Real Power Is Here

The columns array is deceptively simple on the surface. Each entry can be
a plain string (just the column key), but once you switch to the full object syntax,
you unlock per-column control over sorting behavior, filter type, display logic,
and — most importantly —
custom rendering.
This is where enterprise requirements start to feel manageable rather than terrifying.

The options property within each column definition accepts a
customBodyRender function. This function receives the cell value and the
full row metadata object, and returns any valid React node. You can render badges, avatars,
action buttons, progress bars, or entirely composed sub-components — no restrictions,
no wrappers needed. The column stays sortable and filterable based on the underlying data
value, while the visual output is entirely yours to control.

const columns = [
  {
    name: "status",
    label: "Status",
    options: {
      filter: true,
      sort: true,
      customBodyRender: (value) => {
        const colorMap = {
          active:   "#4caf50",
          inactive: "#f44336",
          pending:  "#ff9800",
        };
        return (
          <span style={{
            background: colorMap[value] || "#9e9e9e",
            color: "#fff",
            borderRadius: "12px",
            padding: "2px 12px",
            fontSize: "0.78rem",
            fontWeight: 600,
            textTransform: "capitalize",
          }}>
            {value}
          </span>
        );
      },
    },
  },
  {
    name: "actions",
    label: "Actions",
    options: {
      filter: false,
      sort: false,
      customBodyRender: (value, tableMeta) => {
        const rowId = tableMeta.rowData[0]; // assumes ID is column 0
        return (
          <button onClick={() => handleEdit(rowId)}>Edit</button>
        );
      },
    },
  },
];

Notice the use of tableMeta.rowData in the actions column above. This array
contains the raw cell values for the entire row, in column order. It’s the primary way to
access sibling column data inside a custom renderer — and it’s worth understanding early,
because nearly every non-trivial
mui-datatables custom rendering
use case depends on it.


Filtering, Search, and the Options Object in Depth

Out of the box,
mui-datatables filtering
supports four modes: checkbox, dropdown, multiselect,
and textField. You set the global default via filterType in the
top-level options, and you can override it per column using the column’s own
options.filterType. This granularity is one of those features you don’t
appreciate until you need a text filter on one column and a multiselect on another.

The global search bar operates as a client-side full-text filter across all visible columns
by default. If you need to customize which columns participate in search, or if you need to
debounce the input for performance reasons, you can intercept the behavior via
onSearchChange and onFilterChange callbacks.
For truly custom filter logic — say, a date range picker — the library exposes
customFilterListOptions and filterOptions.logic at the column
level, giving you full control over how filter values are applied against row data.

One underused but powerful option is confirmFilters. When set to
true, filters are not applied immediately on change — instead, they queue up
until the user explicitly clicks a confirm button. For server-side scenarios where every
filter change triggers an API call, this single option can dramatically reduce unnecessary
network traffic and improve perceived performance. Pair it with
mui-datatables pagination
callbacks and you have a complete request lifecycle you can control programmatically.


Pagination: Client-Side and Controlled Modes

Pagination in
mui-datatables
works in two fundamentally different modes, and picking the right one upfront will save
you a painful refactor later. In client-side mode — the default — you pass the entire
dataset to the component, and it handles slicing, sorting, and page navigation internally.
This is perfect for datasets under roughly 2,000 rows, where the overhead of rendering
everything in memory is negligible.

For larger datasets, controlled pagination is the way to go. You set
page and rowsPerPage explicitly in the options, and respond
to the onChangePage and onChangeRowsPerPage callbacks to
update your component’s state. The table renders only what you give it, and page changes
trigger your state update function, not any internal library logic.
This controlled approach is what enables seamless integration with Redux, Zustand,
React Query, or any other state management layer.

Pagination also participates in the table’s overall accessibility story.
The component renders a proper <nav> element with ARIA labels and
keyboard navigation support. If your organization has WCAG 2.1 compliance requirements,
this matters more than it might seem — and it’s one area where
React enterprise table
solutions often cut corners that mui-datatables doesn’t.


Server-Side Integration: The Production Pattern

Server-side mode is where
mui-datatables server-side
really earns its place in enterprise applications. The core idea is simple: instead of
letting the library manage sorting and filtering against a local array, you delegate those
operations to your backend and feed the component only the current page of results.
The component becomes a pure display layer. Your API becomes the source of truth.

Enabling this requires three changes to your options object:
set serverSide: true, provide the total record count via count,
and attach an onTableChange callback that fires whenever the user sorts,
filters, searches, or navigates pages. The callback receives an action string
(e.g., "sort", "filterChange", "changePage") and
a full state object containing the current sort column, filter values, page index, and
rows-per-page setting. You use those values to build your API query.

const [data,    setData]    = React.useState([]);
const [count,   setCount]   = React.useState(0);
const [loading, setLoading] = React.useState(false);

const fetchData = async (tableState) => {
  setLoading(true);
  const { page, rowsPerPage, sortOrder, filterList, searchText } = tableState;

  const response = await fetch("/api/users?" + new URLSearchParams({
    page:         page,
    limit:        rowsPerPage,
    sortField:    sortOrder.name      || "id",
    sortDir:      sortOrder.direction || "asc",
    search:       searchText          || "",
    // filterList is an array-of-arrays aligned to column indices
  }));

  const json = await response.json();
  setData(json.records);
  setCount(json.total);
  setLoading(false);
};

const options = {
  serverSide:    true,
  count:         count,
  onTableChange: (action, tableState) => {
    // Avoid infinite loops — only fetch on actual data-affecting events
    if (["sort", "filterChange", "search",
         "changePage", "changeRowsPerPage"].includes(action)) {
      fetchData(tableState);
    }
  },
  textLabels: {
    body: { noMatch: loading ? "Loading…" : "No records found." },
  },
};

One important nuance: onTableChange fires for every table event,
including things like column visibility toggles that don’t require a network call.
The conditional check around the action string shown above isn’t optional — without it,
you’ll fire API requests on events that have nothing to do with your data, which is both
wasteful and occasionally buggy in subtle ways.


Advanced Customization: Toolbars, Row Expansion, and Theme Overrides

The standard toolbar renders a title, search icon, filter icon, column visibility toggle,
and download button. That covers 80% of use cases, but for the other 20%, the library
exposes customToolbar and customToolbarSelect options.
The former lets you inject custom React elements into the toolbar (a date range picker,
a „Create New” button, an export dropdown); the latter replaces the toolbar that appears
when rows are selected, which is useful for bulk-action patterns.

Row expansion is handled through expandableRows: true combined with the
renderExpandableRow option. The render function receives the row’s raw data
and metadata, and returns a full-width <TableRow> containing whatever
secondary content you need — a detail panel, a nested table, a chart, a form.
This pattern is particularly effective for master-detail UIs where you want to keep the
primary list compact while making related information accessible on demand.

For theming, the library respects MUI’s ThemeProvider and also accepts
a components override object for surgical adjustments.
If you need to change the paper elevation, table row hover color, or header font weight
without touching your global theme, you can wrap MUIDataTable in a scoped
ThemeProvider that extends your base theme with table-specific overrides.
This keeps your component styles isolated and avoids the cascading style conflicts that
plague large design systems.

  • customToolbar — inject action buttons or pickers into the top bar
  • renderExpandableRow — build master-detail layouts with nested content
  • setRowProps — apply inline styles or class names conditionally per row (highlight overdue items, flag anomalies)
  • customFooter — replace the default pagination UI with a fully custom component

Performance at Scale: What You Need to Know Before You Ship

Client-side mode with several thousand rows will eventually produce noticeable re-render
lag, especially when filters or sort operations touch every row. The most effective
mitigation is memoization: wrap your columns array in useMemo
and your callback functions in useCallback so that the component doesn’t
treat them as new references on every parent render. This single change accounts for
the majority of avoidable re-renders in typical usage.

For datasets that genuinely require virtual scrolling (tens of thousands of rows in view
simultaneously), mui-datatables on its own won’t be sufficient —
at that scale, you’d want to pair server-side mode with aggressive pagination
(25–50 rows per page) rather than attempting to virtualize the DOM.
The library doesn’t include built-in row virtualization, which is a fair trade-off
given that most real-world enterprise tables use pagination precisely to avoid
rendering thousands of DOM nodes simultaneously.

Bundle size is worth considering too. The full
React advanced data table
package adds roughly 50–70KB gzipped to your bundle, which is competitive with alternatives
of similar feature depth. If you’re tree-shaking MUI correctly and using dynamic imports
for route-level code splitting, the impact on initial load time is minimal —
typically well under 200ms on a median connection speed.


Putting It Together: A Complete Production-Ready Example

The following example combines server-side pagination, custom column rendering,
controlled filters, and a custom toolbar into a single, deployable component.
It represents the architecture used in real enterprise dashboards — the kind where
product managers add requirements every sprint and the table still needs to hold together.

import React, { useEffect, useState, useCallback, useMemo } from "react";
import MUIDataTable from "mui-datatables";
import { Button, Chip } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";

const PRIORITY_COLORS = {
  high:   "error",
  medium: "warning",
  low:    "success",
};

export default function TasksTable({ onCreateTask }) {
  const [data,    setData]    = useState([]);
  const [count,   setCount]   = useState(0);
  const [loading, setLoading] = useState(true);

  const fetchTasks = useCallback(async (tableState = {}) => {
    setLoading(true);
    const {
      page         = 0,
      rowsPerPage  = 10,
      sortOrder    = {},
      searchText   = "",
    } = tableState;

    try {
      const res  = await fetch(`/api/tasks?page=${page}&limit=${rowsPerPage}&sort=${sortOrder.name || "created_at"}&dir=${sortOrder.direction || "desc"}&q=${searchText}`);
      const json = await res.json();
      setData(json.items);
      setCount(json.total);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { fetchTasks(); }, [fetchTasks]);

  const columns = useMemo(() => [
    { name: "id",       label: "ID",       options: { filter: false } },
    { name: "title",    label: "Task",     options: { filter: false } },
    {
      name: "priority",
      label: "Priority",
      options: {
        filterType: "multiselect",
        customBodyRender: (val) => (
          <Chip
            label={val}
            color={PRIORITY_COLORS[val] || "default"}
            size="small"
            sx={{ textTransform: "capitalize", fontWeight: 700 }}
          />
        ),
      },
    },
    { name: "assignee",   label: "Assignee"   },
    { name: "due_date",   label: "Due Date"   },
    {
      name: "id",
      label: "Actions",
      options: {
        filter: false,
        sort:   false,
        customBodyRender: (id) => (
          <Button size="small" variant="outlined" href={`/tasks/${id}`}>
            View
          </Button>
        ),
      },
    },
  ], []);

  const options = useMemo(() => ({
    serverSide:    true,
    count,
    rowsPerPage:   10,
    rowsPerPageOptions: [10, 25, 50],
    selectableRows: "none",
    filterType:    "multiselect",
    responsive:    "vertical",
    onTableChange: (action, state) => {
      if (["sort","filterChange","search","changePage","changeRowsPerPage"].includes(action)) {
        fetchTasks(state);
      }
    },
    customToolbar: () => (
      <Button
        startIcon={<AddIcon />}
        variant="contained"
        size="small"
        onClick={onCreateTask}
        sx={{ ml: 1 }}
      >
        New Task
      </Button>
    ),
    textLabels: {
      body: { noMatch: loading ? "Fetching tasks…" : "No tasks found." },
    },
  }), [count, fetchTasks, loading, onCreateTask]);

  return (
    <MUIDataTable
      title="Task Management"
      columns={columns}
      data={data}
      options={options}
    />
  );
}

This component is effectively a self-contained feature module. It manages its own data
fetching lifecycle, exposes a single onCreateTask prop for parent
integration, and handles all table events internally. Adding a new column or filter
type is a matter of extending the columns array — the rest of the
architecture doesn’t need to change.

Pro tip: Always wrap columns and options
in useMemo. mui-datatables performs deep equality checks on these props,
but React’s reconciler will still trigger unnecessary effects if your references change
on every render. Memoization here is not premature optimization — it’s table hygiene.

Frequently Asked Questions

How do I install and set up mui-datatables in a React project?

Run npm install mui-datatables @mui/material @emotion/react @emotion/styled
from your project root. Then import MUIDataTable from
'mui-datatables' and render it with three required props:
title, columns (array of column definition objects),
and data (array of row objects or 2D array). Add an
options prop to configure filtering, pagination, and selection behavior.
The component works immediately without any additional CSS imports or provider wrappers —
though it does need to live inside MUI’s ThemeProvider if you’re
using a custom theme.

How does server-side pagination work in mui-datatables?

Set serverSide: true in your options object and provide the total record
count via the count option. Then attach an onTableChange
callback — this fires whenever the user changes page, sorts a column, applies a filter,
or searches. Inside the callback, read the current tableState object
to extract pagination and sort parameters, use them to build your API request,
and update your component state with the response. The table will re-render with
the new data automatically. Critically, filter the action string inside
onTableChange to avoid triggering network calls on non-data events like
column visibility changes.

What is the difference between mui-datatables and MUI DataGrid?

MUI DataGrid is the official MUI team product, well-maintained and tightly integrated
with the MUI ecosystem — but advanced features like row grouping, column pinning,
and Excel export require a paid Pro or Premium license.
mui-datatables is a fully open-source, MIT-licensed community library built on top of
MUI v5 that ships those kinds of features (custom rendering, advanced filtering,
server-side mode, expandable rows) without any licensing cost.
DataGrid has a more modern API and better virtualization for very large datasets;
mui-datatables has more flexibility for custom UI patterns and is the better choice
when licensing constraints are a factor.


Sources & further reading:

Advanced Data Table Implementation with mui-datatables in React — DevFoundryXT