Material React Table: Complete Guide to Setup, Features and Advanced Usage
If you’ve spent more than twenty minutes wiring up a sortable, filterable, paginated table from scratch in React,
you already know the pain. Material React Table
exists precisely to end that suffering — and it does so without locking you into an opinionated black box.
This guide walks you from zero to a production-ready React data table with Material-UI styling,
covering every feature you’ll actually need.
What Is Material React Table and Why Should You Care?
Material React Table (MRT)
is a fully-featured React table component built on two powerhouses:
TanStack Table v8 for headless logic
and MUI (Material-UI) v5 for the visual layer.
The result is a React data grid that ships with sorting, filtering, pagination, row selection,
column resizing, virtualization, and nested row expansion — all configurable through a single, well-typed API.
Calling it a „table component” undersells it; it’s closer to a data management system that renders as a table.
The architectural choice to sit on top of TanStack Table is significant. TanStack Table is headless by design,
meaning it carries zero UI opinions. MRT layers MUI components over that headless core, so every cell, header,
toolbar button, and pagination control is a real MUI component you can override via the standard
sx prop or muiTableProps-style escape hatches. You’re not fighting a closed system —
you’re extending an open one. This is exactly what distinguishes MRT from heavier, less flexible alternatives
when building a React enterprise data table.
Compare this to rolling your own solution with a plain React Material-UI table: the base
The material-react-table installation is straightforward, but it requires a handful of peer Run the full install command in one shot:
If your project uses Yarn or pnpm, swap the package manager prefix — the dependency list stays the same.
One gotcha worth noting upfront: MRT v2 requires React 18 and MUI v5. If your project is still on React 17
Theory is fine; a working material-react-table example is better. The minimal viable table
That’s a fully functional, sortable, globally-filterable table. No extra configuration needed.
The
Out of the box, Material React Table sorting is enabled on every column that has an
Material React Table filtering is where things get genuinely impressive. Each column gets
Material-react-table pagination sits at the bottom of the table by default, with a rows-per-page
A React table component advanced use case typically involves some combination of row selection,
Column pinning — the ability to freeze columns on the left or right while the rest of the table scrolls
Virtualization is the performance lever you reach for when your dataset grows beyond a few hundred rows.
The default MRT toolbar includes a global search input, a filter toggle, a column visibility menu, and a
Custom cell renderers are where MRT’s TypeScript integration pays off most visibly. The
For editable tables — a common pattern in admin interfaces — MRT provides a built-in editing mode switchable
Client-side sorting and filtering are convenient for small datasets, but the real-world scenario for a
Pairing this with TanStack Query
One pattern worth establishing early in any server-side implementation: debounce the global filter. The
Since MRT renders real MUI components, it inherits your application’s MUI theme automatically. If your project
Accessibility deserves explicit mention because data tables are notoriously difficult to get right for screen
For fine-grained style overrides that go beyond your MUI theme, every structural element in MRT has a
Run
Yes, fully. Set
MUI’s
component from MUI is a presentational primitive. You’d spend a sprint writing
sort state machines, filter debouncing, pagination arithmetic, and column pinning logic before writing a single
line of actual product feature. MRT collapses all of that into configuration. The tradeoff is a larger bundle,
but in any data-heavy application that tradeoff is laughably worth it.
Material React Table Installation and Project Setup
dependencies that catch people off guard. MRT relies on MUI’s core, icon set, and date pickers package,
plus the Emotion CSS-in-JS engine that MUI v5 uses under the hood. Missing any one of these will produce
cryptic runtime errors that send you on a twenty-minute debugging detour. Save yourself the time.
npm install material-react-table \
@mui/material \
@mui/x-date-pickers \
@mui/icons-material \
@emotion/react \
@emotion/styled
TypeScript users get type definitions bundled inside material-react-table itself; there’s no
separate @types package to hunt for. Once the install completes, your material-react-table setup
is functionally done. There’s no Webpack plugin, no Babel transform, and no CSS import required — MUI handles
all styling through Emotion at runtime. You can verify the install worked by importing the component and
checking for TypeScript autocomplete on the columns prop.
or MUI v4, you’ll need to either upgrade or pin to MRT v1. The v2 API is meaningfully cleaner, so the upgrade
is worth the effort if you have any flexibility in your dependency versions. The
official install docs
call this out explicitly and provide a migration checklist.
Your First Material React Table Example
needs exactly three things: a column definition array, a data array, and the MaterialReactTable
component. No context providers, no global store, no theme injection (though you can add one). Here’s a
complete, copy-pasteable example that renders a user list with three columns:
import { useMemo } from 'react';
import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table';
type User = {
id: number;
name: string;
email: string;
role: string;
};
const data: User[] = [
{ id: 1, name: 'Alice Monroe', email: 'alice@example.com', role: 'Admin' },
{ id: 2, name: 'Bob Tanaka', email: 'bob@example.com', role: 'Editor' },
{ id: 3, name: 'Carol Osei', email: 'carol@example.com', role: 'Viewer' },
{ id: 4, name: 'David Reyes', email: 'david@example.com', role: 'Editor' },
];
export default function UserTable() {
const columns = useMemo
The useMemo around the column definition is important — if you define columns inline without
memoization, React recreates the array every render, causing unnecessary re-renders inside MRT’s
internal diffing. It’s a small thing, but in a table with hundreds of rows it shows up in profiler traces.
MRT_ColumnDef generic gives you end-to-end type safety: TypeScript will error if
accessorKey doesn’t match a key in your data type, if a Cell renderer
uses the wrong value type, or if you pass an unknown prop to the table. This kind of tight typing is
exactly what makes MRT a serious choice for a React interactive table in a typed codebase.
The developer experience here is genuinely good.
Sorting, Filtering, and Pagination Deep Dive
accessorKey or accessorFn. Clicking a column header cycles through ascending,
descending, and unsorted states. Multi-column sort works by holding Shift while clicking — a
behavior most users expect from spreadsheet tools and which MRT provides without any configuration.
To disable sorting on a specific column, set enableSorting: false in that column’s definition.
To disable it globally, pass enableSorting={false} to the table itself.
a per-column filter input in the header row (toggle it with the filter icon in the toolbar), and the global
search box filters across all columns simultaneously. The filter mode can be set per column:
filterVariant: 'select' renders a dropdown, 'range' gives you a min/max pair for
numeric columns, 'date-range' integrates with MUI X Date Pickers, and 'multi-select'
allows multiple values. You choose the filter variant that matches the data semantics, not the one the library
chose for you. This level of granularity is rare without going full custom.
selector and page navigation. The page sizes array is configurable via muiPaginationProps.
For server-side pagination — the pattern you’ll need in any table with more than a few
thousand rows — you set manualPagination={true}, pass rowCount as the total server-side
count, and wire onPaginationChange to a state setter that triggers your API call. MRT hands you
the current {pageIndex, pageSize} state; you pass it to your query function; you get back data.
The handoff is clean and predictable, which matters enormously in production systems where the data layer
is your single source of truth.
Advanced React Table Patterns for Enterprise Use
row expansion, column pinning, and virtualization. MRT handles all four. Row selection is enabled with
enableRowSelection={true} and exposes a rowSelection state object keyed by row ID.
You can restrict selection to specific rows via a function: enableRowSelection={(row) => row.original.status !== 'locked'}.
The selected rows are accessible through the table instance, making it trivial to build bulk-action toolbars
that operate on the current selection.
horizontally — is a feature most custom table implementations skip because it’s genuinely painful to build.
In MRT, you set enableColumnPinning={true} and either let users pin columns via the column
actions menu, or pin programmatically via initialState: { columnPinning: { left: ['name'] } }.
For very wide tables with many optional columns, this feature alone justifies the library choice. Pair it with
column resizing (enableColumnResizing={true}) and you have a data grid that feels as capable as
a desktop spreadsheet application inside a browser tab.
MRT integrates TanStack Virtual
directly: set enableRowVirtualization={true} and the table renders only the rows visible in the
viewport, recycling DOM nodes as the user scrolls. The implementation detail here matters: row virtualization
requires a fixed row height for accurate scroll-position math. If your rows have variable heights — think
expandable content areas — you’ll need to specify rowVirtualizerOptions={{ estimateSize: () => 56 }}
and accept that scroll estimation is approximate. For uniform-height rows, it’s bulletproof. In one
documented enterprise implementation,
a 50,000-row dataset rendered without perceptible lag using this approach — which is frankly the bar any
serious React enterprise data table needs to clear.
Customizing the MRT Toolbar and Cell Renderers
density selector. That’s a reasonable default for most internal tools. For product-facing applications,
you’ll want to inject your own controls. MRT provides renderTopToolbarCustomActions and
renderBottomToolbarCustomActions props that receive the table instance and return JSX. Drop any
MUI component in there — a Button that triggers a CSV export, a Select that switches between saved filter
presets, or a status badge showing the total filtered count. Since you have the table instance, you can call
table.getFilteredRowModel().rows to compute that count in real time without any external state.
Cell
property in a column definition receives a typed render context: ({ cell, row, table }) => ReactNode.
The cell.getValue() call returns the correctly typed value for that column — a string, number,
date, or whatever your data model defines. You can return any JSX: a MUI Chip for status fields,
a formatted currency string for financial columns, a clickable avatar for user name columns. The render
context also exposes row.original, giving you access to the full row data for cases where the
display logic depends on sibling fields.
between 'cell', 'row', 'table', and 'modal' variants.
Set enableEditing={true}, choose an editDisplayMode, and provide an
onEditingRowSave handler that receives the updated row values and the exit callback. The row-level
mode shows Save/Cancel buttons inline; the modal mode opens a MUI Dialog with a generated form. Both are
functional with zero additional UI code. If you need custom form fields inside the modal, override specific
columns with Edit: ({ cell }) => . The escape hatch is always available.
Server-Side Operations: The Production-Ready Pattern
React data grid Material-UI implementation usually involves an API that owns the data.
MRT’s manual mode covers all three main operations independently: you can have manual pagination with
client-side sorting, or all three manual simultaneously. The pattern is consistent across all three:
set manual[Feature]={true}, provide a change handler, and derive your API query parameters
from the resulting state. Here’s the core structure for a fully server-driven table:
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 });
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([]);
const [globalFilter, setGlobalFilter] = useState('');
const { data, isLoading, isError } = useQuery({
queryKey: ['users', pagination, sorting, columnFilters, globalFilter],
queryFn: () => fetchUsers({ pagination, sorting, columnFilters, globalFilter }),
});
// Inside your MaterialReactTable props:
// manualPagination manualSorting manualFiltering
// onPaginationChange onSortingChange onColumnFiltersChange
// rowCount={data?.total}
(React Query) gives you caching, background refetching, and loading states with minimal boilerplate.
MRT exposes state={{ isLoading }} which renders skeleton rows while your query is in flight —
a small but user-experience-significant detail. Setting state={{ showAlertBanner: isError }}
renders an error banner at the top of the table automatically. These built-in state integrations mean
your data-fetching logic stays in your query layer, not scattered across table props.
onGlobalFilterChange fires on every keystroke by default, which would hammer your API on every
character. Wrap your state setter in a debounce utility — lodash.debounce or a custom hook —
and delay the actual state update by 300–400ms. The column filters don’t need this treatment since users
typically commit them deliberately, but free-text search fields are typed continuously and need the protection.
It’s a two-line fix that prevents a class of unnecessary API load that would otherwise only surface under
production traffic.
Theming, Accessibility, and MUI Integration
already has a ThemeProvider wrapping the app with custom palette, typography, and component
overrides, MRT respects all of it without any extra configuration. The table renders in your brand colors,
with your font stack, using your defined component variants. This is the practical benefit of building on
top of an established design system rather than reimplementing one. For dark mode, you simply switch the
MUI theme’s palette.mode to 'dark' and MRT adapts the table, toolbar, modals,
and filter dropdowns consistently.
readers. MRT outputs semantic ,
,
,
and
elements with correct ARIA roles, aria-sort on sortable column
headers, and aria-label on interactive controls. Keyboard navigation through the table is
handled, including focus management during column sorting and row selection. This isn’t just a checkbox
for compliance — screen reader users in enterprise environments are a real demographic, and shipping a table
that breaks with a screen reader is a product defect, not a nice-to-have issue.
corresponding mui[Element]Props prop. muiTableHeadCellProps targets header cells,
muiTableBodyCellProps targets body cells, and both accept either a static props object or a
function that receives the cell context — letting you apply conditional styles based on cell value, row index,
or selection state. If you need to highlight overdue dates in red or bold the maximum value in a numeric column,
this is the correct mechanism. It keeps styling logic co-located with data logic and avoids the fragile global
CSS selectors that tend to accumulate in long-lived frontend codebases.
Frequently Asked Questions
npm install material-react-table @mui/material @mui/x-date-pickers @mui/icons-material @emotion/react @emotion/styled.
Then import MaterialReactTable from 'material-react-table', define your columns
with useMemo, pass your data array, and render the component. No CSS imports, no plugins,
no theme provider required for the basic setup — though a ThemeProvider is recommended
for any production application.
manualPagination and manualFiltering to true,
then wire onPaginationChange and onColumnFiltersChange to state setters that
trigger your API calls. Pass rowCount as the total record count from your server so MRT
can render the correct number of pagination pages. The library owns no data — your backend does.
is a presentational primitive — it renders HTML table markup with MUI
styling and nothing else. You build all interactive behavior yourself. Material React Table is a fully
featured React data grid built on TanStack Table v8 and MUI, shipping with sorting,
filtering, pagination, row selection, column resizing, virtualization, and editing modes out of the box.
Use MUI Table when you need a simple, static layout. Use MRT when your table needs to do real work.
Share This Story, Choose Your Platform!