A data grid is a table that sorts, filters, paginates and selects rows for you. In React the usual choice is MUI X DataGrid, whose Community version is free and MIT licensed, and this guide builds a working grid with it: columns, pagination, sorting, checkbox selection, formatted and custom cells. It also answers the question that brings most people here, disableSelectionOnClick, which was renamed in v6 and no longer exists under that name. At the end there is a plain table for the cases where a grid library is more than you need.
Everything here was built and rendered with React 19.3.0, MUI 9.4.0 and @mui/x-data-grid 9.14.0 in a Vite project, and the screenshots are the real grid in a browser. Reference: the MUI X Data Grid documentation.
Install and render a React data grid
Two packages plus the styling engine MUI uses. The grid needs a parent with a height, because it virtualises its rows and has to know how tall it is:
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/x-data-grid
Every row needs a unique id field, or you pass getRowId to point at your own key. Columns are plain objects, and type: "number" is what makes sorting and alignment behave:
import { DataGrid } from "@mui/x-data-grid";
const rows = [
{ id: 1, customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped" },
{ id: 2, customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending" },
{ id: 3, customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped" },
{ id: 4, customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled" },
];
const columns = [
{ field: "customer", headerName: "Customer", width: 170 },
{ field: "city", headerName: "City", width: 120 },
{ field: "total", headerName: "Total", type: "number", width: 110 },
{ field: "status", headerName: "Status", width: 120 },
];
export default function App() {
return (
<div style={{ height: 320, width: "100%", fontFamily: "system-ui", padding: 8 }}>
<DataGrid
rows={rows}
columns={columns}
initialState={{ pagination: { paginationModel: { pageSize: 5, page: 0 } } }}
pageSizeOptions={[5, 10]}
/>
</div>
);
}
| You want | Prop |
|---|---|
| Page size and starting page | initialState.pagination.paginationModel |
| Page size choices | pageSizeOptions={[5, 10]} |
| Checkboxes on each row | checkboxSelection |
| Clicking a cell must not select | disableRowSelectionOnClick |
| React to selection | onRowSelectionModelChange |
| Format a value | valueFormatter on the column |
| Render your own JSX | renderCell on the column |
| A computed column | valueGetter with no matching field |
disableSelectionOnClick: the prop that was renamed
If you copied an older example, disableSelectionOnClick simply does nothing now: it became disableRowSelectionOnClick in v6, along with selectionModel becoming rowSelectionModel and onSelectionModelChange becoming onRowSelectionModelChange. Here it is on a grid with checkboxes, where clicking a cell leaves the selection alone:
import { useState } from "react";
import { DataGrid } from "@mui/x-data-grid";
const rows = [
{ id: 1, customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped" },
{ id: 2, customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending" },
{ id: 3, customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped" },
{ id: 4, customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled" },
];
const columns = [
{ field: "customer", headerName: "Customer", width: 170 },
{ field: "city", headerName: "City", width: 120 },
{ field: "total", headerName: "Total", type: "number", width: 110 },
];
export default function App() {
const [selected, setSelected] = useState([]);
return (
<div style={{ fontFamily: "system-ui", padding: 8 }}>
<p id="count">selected: {selected.length}</p>
<div style={{ height: 300, width: "100%" }}>
<DataGrid
rows={rows}
columns={columns}
checkboxSelection
disableRowSelectionOnClick // clicking a cell no longer selects the row
onRowSelectionModelChange={(model) => {
const ids = Array.from(model?.ids ?? model ?? []);
setSelected(ids);
console.log("selection:", ids);
}}
/>
</div>
</div>
);
}
Console output after clicking a row checkbox:
selection: [1]
Two more renames worth knowing while you are there: rowsPerPageOptions is now pageSizeOptions, and page with pageSize moved into the single paginationModel object shown above.
Formatting and custom cells
valueFormatter changes what is displayed while sorting still uses the underlying number, which is why you should not format the value in your data. renderCell returns JSX for badges, buttons and links, and valueGetter builds a column that has no field of its own, the same way a derived value works anywhere else:
import { DataGrid } from "@mui/x-data-grid";
const rows = [
{ id: 1, customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped" },
{ id: 2, customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending" },
{ id: 3, customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped" },
{ id: 4, customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled" },
];
const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
const colors = { shipped: "#0b6bcb", pending: "#f0a202", cancelled: "#8a1f1f" };
const columns = [
{ field: "customer", headerName: "Customer", width: 170 },
{
field: "total",
headerName: "Total",
type: "number",
width: 120,
valueFormatter: (value) => money.format(value), // format, keep the number for sorting
},
{
field: "status",
headerName: "Status",
width: 130,
renderCell: (params) => ( // your own JSX in the cell
<span style={{ color: colors[params.value], fontWeight: 600 }}>{params.value}</span>
),
},
{
field: "summary",
headerName: "Summary",
width: 220,
valueGetter: (value, row) => `${row.customer} (${row.city})`, // a column with no field
sortable: false,
},
];
export default function App() {
return (
<div style={{ height: 320, width: "100%", fontFamily: "system-ui", padding: 8 }}>
<DataGrid rows={rows} columns={columns} hideFooterSelectedRowCount />
</div>
);
}
valueFormatter, colours from renderCell, and a column with no field behind it.Keep renderCell cheap: it runs for every visible cell on every render. Heavy work belongs in the data, not in the cell.
When you do not need a grid library
MUI X DataGrid is around 100 KB before the MUI packages it depends on. If the table is read-only, or all you need is sorting a handful of rows, a plain <table> with a sort of your own is a few lines and no dependency. The screenshot was taken after a real click on the Customer header:
import { useMemo, useState } from "react";
const rows = [
{ id: 1, customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped" },
{ id: 2, customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending" },
{ id: 3, customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped" },
{ id: 4, customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled" },
];
// a plain table: no dependency, and enough for a read-only grid
export default function App() {
const [sort, setSort] = useState({ key: "total", asc: false });
const sorted = useMemo(() => {
const factor = sort.asc ? 1 : -1;
return [...rows].sort((a, b) =>
(typeof a[sort.key] === "number" ? a[sort.key] - b[sort.key] : String(a[sort.key]).localeCompare(String(b[sort.key]))) * factor,
);
}, [sort]);
const headers = ["customer", "city", "total"];
return (
<table style={{ fontFamily: "system-ui", borderCollapse: "collapse", margin: 8 }}>
<thead>
<tr>
{headers.map((key) => (
<th key={key} id={`th-${key}`} onClick={() => setSort(s => ({ key, asc: s.key === key ? !s.asc : true }))}
style={{ cursor: "pointer", textAlign: "left", padding: "8px 16px",
borderBottom: "2px solid #0b6bcb", textTransform: "capitalize" }}>
{key}{sort.key === key ? (sort.asc ? " \u2191" : " \u2193") : ""}
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((row) => (
<tr key={row.id}>
<td style={{ padding: "6px 16px", borderBottom: "1px solid #e3e8ef" }}>{row.customer}</td>
<td style={{ padding: "6px 16px", borderBottom: "1px solid #e3e8ef" }}>{row.city}</td>
<td style={{ padding: "6px 16px", borderBottom: "1px solid #e3e8ef" }}>${row.total.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
);
}
Move to a grid library when you need virtualisation for thousands of rows, column resizing and reordering, inline editing, grouping or export. Those are the features that take weeks to hand-build and minutes to configure.
| Library | Licence | Notes |
|---|---|---|
| MUI X DataGrid (Community) | MIT, free | Sorting, filtering, pagination, selection. Pro and Premium add grouping, pivoting and export. |
| react-data-grid | MIT, free | Lighter, spreadsheet-like editing, fewer batteries included. |
| AG Grid (Community) | MIT, free | Very feature-rich; the enterprise features are paid. |
| TanStack Table | MIT, free | Headless: you write the markup, it does the logic. |
| A plain table | Free | Read-only lists and small data sets. |
More React component guides worth reading:
- Build a React pivot table component
- Build a React Gantt chart component
- Build a React date picker component
- Build a React JSON viewer component
- MUI table pagination in React
Frequently asked questions
What is the best React data grid component?
MUI X DataGrid for most applications: the Community version is free and covers sorting, filtering, pagination and selection out of the box.
Why does disableSelectionOnClick not work?
It was renamed disableRowSelectionOnClick in v6. The old name is simply ignored, so the grid keeps selecting rows on click.
Why is my MUI DataGrid not showing?
It needs a parent with a height, and every row needs a unique id. Give the wrapper a height or use autoHeight.
How do I get the selected rows?
Handle onRowSelectionModelChange and keep the ids in state, then map them back to your rows when you need the objects.
How do I format a column without breaking sorting?
Use valueFormatter, which only changes the display. Formatting the data itself turns numbers into strings and sorts them as text.
How do I render a button or a badge in a cell?
Use renderCell on the column and return JSX. Keep it light, because it runs for every visible cell.
Do I need a library for a table in React?
No. For read-only data and simple sorting, a plain <table> is smaller and easier. Use a grid for virtualisation, editing or grouping.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn