React Data Grid Component: MUI X DataGrid and the Alternatives

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>
  );
}
A React data grid built with MUI X DataGrid showing four order rows with sortable column headers and a pagination footer
Sorting, column menus, pagination and the row counter come from the component.
You wantProp
Page size and starting pageinitialState.pagination.paginationModel
Page size choicespageSizeOptions={[5, 10]}
Checkboxes on each rowcheckboxSelection
Clicking a cell must not selectdisableRowSelectionOnClick
React to selectiononRowSelectionModelChange
Format a valuevalueFormatter on the column
Render your own JSXrenderCell on the column
A computed columnvalueGetter 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]
A MUI data grid with checkbox selection where one row is ticked and the count above the grid shows one selected
One checkbox ticked, and the count above the grid updated from the change handler.

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>
  );
}
A MUI data grid showing currency formatted totals, coloured status labels and a computed summary column
Currency from 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>
  );
}
A plain React table with clickable sortable headers showing orders sorted by customer name
No library: click a header, set the sort state, render the sorted copy.

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.

LibraryLicenceNotes
MUI X DataGrid (Community)MIT, freeSorting, filtering, pagination, selection. Pro and Premium add grouping, pivoting and export.
react-data-gridMIT, freeLighter, spreadsheet-like editing, fewer batteries included.
AG Grid (Community)MIT, freeVery feature-rich; the enterprise features are paid.
TanStack TableMIT, freeHeadless: you write the markup, it does the logic.
A plain tableFreeRead-only lists and small data sets.

More React component guides worth reading:

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.

Leave a Comment