MUI X Data Grid: The Only React Data Table Tutorial You Actually Need
Let’s be honest — picking a React data grid library feels like choosing a pizza topping at a place with a 40-item menu. There are dozens of options, most of them perfectly fine, and yet somehow you still end up second-guessing yourself at 2 AM.
MUI X Data Grid cuts through that noise. It lives inside the Material-UI ecosystem, ships with sensible defaults, and gets you from zero to a fully functional, sortable, filterable data table in React without requiring a PhD in configuration theology.
This guide covers everything from installation to real-world usage patterns — pagination, column definitions, row selection, and a few tricks that the official docs bury three levels deep. No fluff, no lorem ipsum screenshots, just working code and honest commentary.
What Is MUI X Data Grid and Why Should You Care?
MUI X Data Grid is a feature-rich React table component built and maintained by the MUI team — the same people behind the wildly popular Material-UI component library. The «X» in the name refers to MUI X, a collection of advanced components that go beyond the base Material-UI set. Unlike a basic HTML table wrapped in React, the DataGrid is engineered specifically for displaying, sorting, filtering, and interacting with structured datasets at scale.
What makes it stand out in a crowded field of React data grid solutions is the combination of zero-config defaults and deep customization hooks. You can drop a <DataGrid> into your component, hand it an array of rows and a column schema, and have something production-presentable in under ten minutes. Or you can spend a week tuning every cell renderer, custom toolbar, and server-side pagination callback — the API is deep enough to support both workflows without making either one painful.
The library ships in three tiers: the free Community edition (@mui/x-data-grid), the paid Pro edition (@mui/x-data-grid-pro), and Premium. For the majority of applications — internal dashboards, admin panels, reporting interfaces — the Community version is genuinely complete. The Pro tier becomes relevant when you need column pinning, multi-column sorting, row grouping, or genuinely large dataset virtualization. This guide focuses on the Community version, which is MIT-licensed and has no gotchas.
<table>. It’s a full-featured React spreadsheet table with virtualization, built-in accessibility, keyboard navigation, and a plugin-style API. If you’re rendering 8 rows of static data, a plain table is fine. If you’re rendering dynamic, user-manipulable data — this is your tool.
MUI X Data Grid Installation: Getting the Environment Right
The MUI X Data Grid installation process is straightforward, but there are a few peer dependency details worth knowing before you hit a wall. The package requires React 17+ and depends on @mui/material, @emotion/react, and @emotion/styled for its styling engine. If you’re already using Material-UI in your project, you likely have all of these. If not, they all install together cleanly.
Open your terminal at the project root and run the following:
# npm
npm install @mui/x-data-grid @mui/material @emotion/react @emotion/styled
# yarn
yarn add @mui/x-data-grid @mui/material @emotion/react @emotion/styled
# pnpm
pnpm add @mui/x-data-grid @mui/material @emotion/react @emotion/styled
That’s the complete dependency footprint for the Community edition. No separate CSS file imports, no PostCSS config, no webpack loader changes — Emotion handles all styling at runtime. If you’re using styled-components instead of Emotion in an existing project, MUI X supports that too, but requires a slightly different setup involving @mui/styled-engine-sc. For green-field projects, stick with Emotion; it’s the default and the path of least resistance.
After installation, verify the package landed correctly by checking your package.json. You should see @mui/x-data-grid listed with a version in the ^6.x or ^7.x range depending on when you’re reading this. Version 7 introduced some breaking changes in column definitions — specifically the field type handling — so if you’re migrating from v5 or v6, skim the migration guide before dropping in your old column configs.
MUI X Data Grid Setup: Building Your First Data Table
With dependencies installed, the MUI X Data Grid setup for a minimal working example is genuinely minimal. The DataGrid component takes two required props: rows (an array of objects, each with a unique id field) and columns (an array of column definition objects). Everything else is optional and defaults to sensible behavior.
Here’s a self-contained example you can paste directly into any React component file:
import React from 'react';
import { DataGrid } from '@mui/x-data-grid';
// Column definitions — the schema of your table
const columns = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'firstName', headerName: 'First Name', width: 150 },
{ field: 'lastName', headerName: 'Last Name', width: 150 },
{ field: 'age', headerName: 'Age', type: 'number', width: 90 },
{
field: 'fullName',
headerName: 'Full Name',
width: 200,
// valueGetter computes a derived value from other fields
valueGetter: (value, row) =>
`${row.firstName || ''} ${row.lastName || ''}`,
},
];
// Row data — each object must have a unique `id` property
const rows = [
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
{ id: 5, lastName: 'Targaryen',firstName: 'Daenerys',age: null},
];
export default function BasicDataGrid() {
return (
<div style={{ height: 400, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
initialState={{
pagination: { paginationModel: { page: 0, pageSize: 5 } },
}}
pageSizeOptions={[5, 10, 25]}
checkboxSelection
disableRowSelectionOnClick
/>
</div>
);
}
A few things worth noting here. First, the wrapping <div> with an explicit height is not optional decoration — the DataGrid requires a constrained height to activate its internal virtualization. Remove it and you’ll get a zero-height grid or a console warning, depending on your version. Second, checkboxSelection adds a checkbox column for row selection with zero additional configuration. Third, disableRowSelectionOnClick prevents the entire row from toggling selection when clicked — which is almost always the right UX choice when your rows contain interactive elements.
Notice that the valueGetter signature changed in MUI X v7: it now receives (value, row, column, apiRef) instead of the older (params) object pattern. If you’re copy-pasting examples from older tutorials and getting undefined in derived columns, this is almost certainly why.
Column Definitions: Where Most of Your Customization Lives
The column definition array is the backbone of any Material-UI data grid implementation. Each column object maps a data field to a rendered cell, and the configuration options available per column are extensive enough to handle nearly any display requirement without writing a custom cell component. Understanding the most useful properties will get you 90% of the way to a polished table.
The type property is one of the most underused. Setting type: 'number', type: 'date', type: 'dateTime', or type: 'boolean' doesn’t just change how the value renders — it also changes the built-in filter operators available for that column. A number column gets «greater than», «less than», and «between» filters for free. A date column gets date-picker-based filtering. A boolean column renders as a checkbox. All of this comes for free when you specify the type correctly, which is a much better deal than writing custom filter logic.
For columns requiring custom rendering — status badges, action buttons, formatted currency — the renderCell property accepts a function that returns JSX. The function receives a params object containing the row data, field value, and grid API reference. Here’s a practical example rendering a colored status chip:
{
field: 'status',
headerName: 'Status',
width: 130,
renderCell: (params) => {
const colorMap = {
active: '#4caf50',
pending: '#ff9800',
inactive: '#f44336',
};
return (
<span style={{
background: colorMap[params.value] || '#9e9e9e',
color: '#fff',
borderRadius: '12px',
padding: '2px 10px',
fontSize: '0.82rem',
fontWeight: 600,
}}>
{params.value}
</span>
);
},
}
The flex property is worth highlighting for responsive layouts. Instead of a fixed width, setting flex: 1 on a column tells the grid to distribute available space proportionally — similar to CSS flexbox. Combining a few fixed-width columns (like an ID or action column) with flex-growing content columns produces a table that behaves gracefully across screen sizes without media query gymnastics.
MUI X Data Grid Pagination: Client-Side and Server-Side
MUI X Data Grid pagination works out of the box the moment you add pageSizeOptions to your component. By default, the grid operates in client-side mode: you hand it the full dataset, and it slices the rows according to the current page and page size. This is perfectly adequate for datasets under a few thousand rows, and with the Community version’s default page size of 100, you can display substantial data without any server involvement.
Controlled pagination — where your application manages the current page state — is equally straightforward. You lift the paginationModel state up and pass it alongside an onPaginationModelChange handler:
import React, { useState } from 'react';
import { DataGrid } from '@mui/x-data-grid';
export default function ControlledPaginationGrid({ rows, columns }) {
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 10,
});
return (
<div style={{ height: 500, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[10, 25, 50]}
/>
</div>
);
}
Server-side pagination is where the real power surfaces for large datasets. Set paginationMode="server", provide the total row count via rowCount, and fetch the appropriate data slice whenever onPaginationModelChange fires. The grid renders the correct pagination controls based on your rowCount without ever needing to load all rows into the client. Pair this with a loading state via the loading prop to display a skeleton overlay during fetches, and you have a production-grade data browsing experience that scales to millions of records.
rowCount to the total number of records from your API response, not the length of the current page. Getting this wrong produces a pagination control that thinks you have fewer pages than you actually do — a silent, maddening bug.
Sorting, Filtering, and the Built-In Toolbar
Column-level sorting is enabled by default in MUI X Data Grid — clicking any column header toggles ascending, descending, and unsorted states sequentially. No props required. Filtering is equally plug-and-play: add the slots={{ toolbar: GridToolbar }} prop and you get a fully functional toolbar with a filter panel, column visibility toggle, density selector, and export button in one import. It’s the kind of thing that would take days to build from scratch and takes approximately one line here.
import { DataGrid, GridToolbar } from '@mui/x-data-grid';
// Inside your component JSX:
<DataGrid
rows={rows}
columns={columns}
slots={{ toolbar: GridToolbar }}
slotProps={{
toolbar: {
showQuickFilter: true,
quickFilterProps: { debounceMs: 300 },
},
}}
/>
The showQuickFilter option adds a global search input to the toolbar — a fast full-table text search that works across all string-type columns simultaneously. The debounceMs keeps it from re-filtering on every keystroke, which matters for larger client-side datasets. For column-specific filters, users can open the filter panel from the toolbar and construct filter conditions using the type-appropriate operators automatically derived from your column type definitions.
For cases where you need to control filtering programmatically — pre-applying filters on load, for example, or syncing filters with URL params for shareable views — the filterModel and onFilterModelChange props give you full controlled-component behavior. The filter model structure is an array of filter items, each specifying a field, operator, and value. This same pattern extends to sort state via sortModel and onSortModelChange, giving you a consistent mental model across all grid behaviors.
Row Selection, Events, and Connecting the Grid to Your App
A data table that can’t communicate with the rest of your application is just a pretty display. The React data grid event system is how the DataGrid feeds data back to your components. The most commonly used callback is onRowSelectionModelChange, which fires with an array of selected row IDs whenever the selection state changes. Pair this with rowSelectionModel for controlled selection, and you can drive downstream UI — action buttons, bulk delete confirmations, detail panels — off the grid’s current selection state.
const [selectedIds, setSelectedIds] = useState([]);
<DataGrid
rows={rows}
columns={columns}
checkboxSelection
rowSelectionModel={selectedIds}
onRowSelectionModelChange={(ids) => setSelectedIds(ids)}
/>
// Downstream usage:
<button
disabled={selectedIds.length === 0}
onClick={() => handleBulkDelete(selectedIds)}
>
Delete {selectedIds.length} selected
</button>
Row click events are handled via onRowClick, which receives a params object containing the full row data. This is the natural hook for navigation — clicking a row to open a detail view, for example — or for populating a side panel with row-specific data. The onCellClick variant is available for cell-level granularity when different columns should trigger different actions.
The grid also exposes an imperative API via the apiRef hook from useGridApiRef. This gives you programmatic control over virtually every aspect of the grid — scrolling to a specific row, programmatically triggering an export, reading the current filter state, or refreshing the row cache after a mutation. For most use cases you won’t need it, but for complex application integrations it’s a genuinely powerful escape hatch that avoids the need to re-architect your data flow.
Real-World Patterns: From Example to Production
The gap between a basic MUI X Data Grid example and a production-ready implementation usually comes down to a handful of recurring patterns. The first is loading state management. When fetching data asynchronously, the loading prop renders an overlay on the grid with an indeterminate progress bar. Combine this with a rows={[]} initial state and you get a clean empty-then-loaded transition without layout shift or flash of empty content.
The second recurring pattern is the custom no-rows overlay. By default, an empty dataset renders a generic «No rows» message. For applications where empty states carry meaning — «No results for your current filters» vs «No data has been added yet» — you’ll want to customize this. The slots.noRowsOverlay prop accepts a React component, so you can render anything from a simple message to an illustrated empty state with a call-to-action button.
The third pattern worth knowing is row height customization. The default row height is 52px, which is generous. For data-dense admin interfaces, density="compact" reduces this to 36px globally. For variable row heights — when cells contain multiline content or embedded images — the getRowHeight prop accepts a function returning a height per row, or the special DataGrid.autoHeight string to let rows size to their content. This last option disables virtualization for those rows, so use it judiciously on large datasets.
Here’s a feature comparison between the three MUI X Data Grid tiers to help you make an informed decision:
| Feature | Community (Free) | Pro | Premium |
|---|---|---|---|
| Sorting & Filtering | ✅ | ✅ | ✅ |
| Pagination | ✅ | ✅ | ✅ |
| Row virtualization | ✅ | ✅ | ✅ |
| Column pinning | ❌ | ✅ | ✅ |
| Row grouping | ❌ | ✅ | ✅ |
| Aggregation | ❌ | ❌ | ✅ |
| Excel export | ❌ | ❌ | ✅ |
| License | MIT | Commercial | Commercial |
Complete Working Example: A Realistic Dashboard Table
Theory only gets you so far. Here’s a more complete MUI X Data Grid example that combines pagination, sorting, filtering, custom cell rendering, and row selection into a single cohesive component — the kind of thing you’d actually ship in a real application:
import React, { useState, useMemo } from 'react';
import { DataGrid, GridToolbar } from '@mui/x-data-grid';
const statusColors = {
Active: { bg: '#e8f5e9', text: '#2e7d32' },
Inactive: { bg: '#fce4ec', text: '#c62828' },
Pending: { bg: '#fff8e1', text: '#f57f17' },
};
const columns = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'name', headerName: 'Name', flex: 1, minWidth: 150 },
{ field: 'email', headerName: 'Email', flex: 1, minWidth: 200 },
{
field: 'revenue',
headerName: 'Revenue',
type: 'number',
width: 130,
valueFormatter: (value) =>
value != null ? `$${value.toLocaleString()}` : '—',
},
{
field: 'status',
headerName: 'Status',
width: 120,
renderCell: (params) => {
const style = statusColors[params.value] || {};
return (
<span style={{
background: style.bg,
color: style.text,
borderRadius: '6px',
padding: '3px 10px',
fontWeight: 600,
fontSize: '0.8rem',
}}>
{params.value}
</span>
);
},
},
{
field: 'joinDate',
headerName: 'Joined',
type: 'date',
width: 130,
valueGetter: (value) => value && new Date(value),
},
];
const mockRows = [
{ id: 1, name: 'Alice Martin', email: 'alice@example.com', revenue: 14200, status: 'Active', joinDate: '2022-03-15' },
{ id: 2, name: 'Bob Chen', email: 'bob@example.com', revenue: 8750, status: 'Pending', joinDate: '2023-07-22' },
{ id: 3, name: 'Carol Davies', email: 'carol@example.com', revenue: 31000, status: 'Active', joinDate: '2021-11-01' },
{ id: 4, name: 'David Kim', email: 'david@example.com', revenue: 0, status: 'Inactive', joinDate: '2020-05-30' },
{ id: 5, name: 'Eva Rossi', email: 'eva@example.com', revenue: 22500, status: 'Active', joinDate: '2023-01-14' },
];
export default function CustomerTable() {
const [selectedIds, setSelectedIds] = useState([]);
const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 5 });
const selectedCount = selectedIds.length;
return (
<div>
{selectedCount > 0 && (
<div style={{ marginBottom: '8px', color: '#1976d2' }}>
{selectedCount} row{selectedCount > 1 ? 's' : ''} selected
</div>
)}
<div style={{ height: 450, width: '100%' }}>
<DataGrid
rows={mockRows}
columns={columns}
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
pageSizeOptions={[5, 10, 25]}
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={selectedIds}
onRowSelectionModelChange={setSelectedIds}
slots={{ toolbar: GridToolbar }}
slotProps={{ toolbar: { showQuickFilter: true } }}
density="comfortable"
/>
</div>
</div>
);
}
This component is genuinely close to production-ready. It handles its own pagination and selection state, applies value formatting to the revenue column, renders typed status badges, and includes the full toolbar with quick search. The main things you’d add for a real deployment are: replacing mockRows with an API call, wrapping the selection buttons in actual action handlers, and potentially adding error boundary handling around the grid if your data fetching can fail.
Common Pitfalls and How to Dodge Them
The single most common issue developers hit with Material-UI data grid integration is the missing height on the wrapper container. The DataGrid uses CSS Grid internally with height: 100%, so it needs its parent to have an explicit height. The fix is always the same: add style={{ height: 400, width: '100%' }} (or whatever height makes sense) to the wrapper div. Using autoHeight is the alternative, but it disables virtualization and can cause performance issues with more than a few hundred rows.
The second pitfall is row identity. Every row object must have an id field by default. If your API returns objects with a different unique identifier — say userId or _id — you have two options: remap your data before passing it to the grid, or use the getRowId prop to tell the grid which field to use as the identity key: getRowId={(row) => row.userId}. Forgetting this produces a warning and potentially broken selection behavior, since the grid can’t track row identity for state management.
The third area of friction is TypeScript integration. The GridColDef type is generic and strict about column types matching row data types. If you’re seeing TypeScript errors around column definitions, check that your valueGetter return type matches what downstream valueFormatter or renderCell expects, and that the type property of your column definition is consistent with the actual data type. The library’s TypeScript support is excellent once you’re aligned with it — it’s just unforgiving when you’re not.
- Zero-height grid? Add explicit
heightto the wrapper container. - Broken selection? Ensure every row has a unique
id, or usegetRowId. - valueGetter not working? Check the v7 signature change:
(value, row)not(params). - Filters not showing right operators? Set the correct
typeon your column definition. - Server pagination showing wrong total? Pass the API’s total count to
rowCount, notrows.length.
Frequently Asked Questions
What is the difference between MUI X Data Grid free and Pro versions?
The free Community version is MIT-licensed and covers sorting, filtering, client- and server-side pagination, row selection, custom cell rendering, and the full toolbar — enough for most production applications. The Pro version adds column pinning, column reordering, tree data, row grouping, and virtualization optimizations for very large datasets. Premium layers on aggregation, Excel export, and advanced chart integrations. If you’re building a standard admin dashboard or reporting table, the Community version will likely never feel limiting.
How do I install MUI X Data Grid in a React project?
Run npm install @mui/x-data-grid @mui/material @emotion/react @emotion/styled from your project root. React 17 or higher is required. If you’re using Yarn, swap npm install for yarn add. No additional configuration is needed — Emotion handles styles at runtime, so there’s no webpack or PostCSS setup required. After installation, import DataGrid directly from @mui/x-data-grid and you’re ready to go.
Can MUI X Data Grid handle large datasets efficiently?
For moderate datasets (under ~10,000 rows), the Community version handles client-side rendering efficiently thanks to row virtualization — only the visible rows are rendered to the DOM at any time. For genuinely large datasets, the right approach is server-side pagination using paginationMode="server", which fetches only the current page from your API. The Pro version extends this with more aggressive virtualization for scenarios where you need all rows in memory but still want smooth scrolling performance.
