Inline cell editing
Mark a column editable and the grid lets users edit its cells in place — double-click a cell, or focus
it and press Enter / F2. The editor is type-aware (text / number / date, and a dropdown for
type: 'select' or type: 'boolean'). Enter or blur commits; Escape cancels.
const columns = [
{ id: 'name', header: 'Name', accessorKey: 'name', editable: true },
{ id: 'role', header: 'Role', accessorKey: 'role', editable: true,
type: 'select', options: [{ label: 'Admin', value: 'Admin' }, { label: 'Editor', value: 'Editor' }] },
];
<DataTable columns={columns} data={rows} editable /* per-column */ />
editable can also be a function — editable: (row) => boolean — to allow editing per row.
Getting the updated data
On commit the grid calls processRowUpdate(newRow, oldRow), then applies the row you return to its
data. This is the one hook you need — it's where you persist and where you can transform or validate:
<DataTable
columns={columns}
data={rows}
processRowUpdate={(newRow, oldRow) => {
// newRow = oldRow with the edited field changed; return the row to apply
return newRow;
}}
onProcessRowUpdateError={(err) => toast.error(String(err))}
/>
- Return a row → the grid applies it (you can normalise/derive fields here, e.g. recompute a
total). - Throw / reject →
onProcessRowUpdateErrorfires and the edit is reverted. - No
processRowUpdate→ edits update the grid's local data directly (handy for prototypes).
Prefer to listen rather than intercept? Use onDataStateChange for view state, or read the data at any time
from apiRef.
Which field gets written
The grid writes to the column's accessorKey (falling back to id), so a column whose id differs from
its accessorKey still updates the right field. Dot-nested accessor keys are deep-set immutably — only the
path is cloned, siblings are preserved:
// accessorKey: 'address.city' → newRow.address.city is updated (address.zip untouched)
{ id: 'city', header: 'City', accessorKey: 'address.city', editable: true }
The edited row keeps its identity via your getRowId (or the idKey prop, default 'id') — that's the id
passed to apiRef.current.data.updateRow, so make it stable if you persist.
Server-side persistence
processRowUpdate may be async — await your API and return the saved row (so server-computed fields flow
back into the grid). Throw to reject and revert:
processRowUpdate={async (newRow, oldRow) => {
const saved = await api.users.update(newRow.id, newRow); // PATCH/PUT
return saved; // grid applies the server's row
}}
This works in server data mode too: the returned row updates the loaded page in
place. If a server-side edit changes sorting/filtering/paging membership, trigger a refetch with
apiRef.current.data.refresh() after it resolves.
Validation
Validate inside processRowUpdate and throw to reject — the grid reverts and surfaces the error:
processRowUpdate={(newRow) => {
if (!newRow.email.includes('@')) throw new Error('Invalid email');
return newRow;
}}
onProcessRowUpdateError={(err) => setError(String(err))}
Read & write data imperatively
Drive data from your own UI through apiRef — the same data API the editor commits through:
apiRef.current?.data.updateRow(rowId, { role: 'Admin' }); // patch one row
apiRef.current?.data.updateField(rowId, 'status', 'active'); // patch one field
apiRef.current?.data.getRowData(rowId); // read a row
apiRef.current?.data.getAllData(); // read all rows
apiRef.current?.data.updateMultipleRows([{ rowId, data: {…} }]); // batch
Whole-row edit mode
Set editMode="row" and a row's editable cells open together, with explicit Save / Cancel in
the actions column (added automatically — no wiring). processRowUpdate fires once with the
fully-updated row:
Click the Edit (pencil) action on a row → every editable cell becomes an input together. Text/number/date
columns show a field; a type: 'select' or type: 'boolean' column shows a dropdown — click it to choose.
Then Save (✓) commits the whole row at once, or Cancel (✕) discards it.
const columns = [
{ id: 'name', header: 'Name', accessorKey: 'name', editable: true },
{ id: 'email', header: 'Email', accessorKey: 'email', editable: true },
// a dropdown editor in the row — opens on click, the choice is saved with the row
{ id: 'role', header: 'Role', accessorKey: 'role', editable: true,
type: 'select', options: [
{ label: 'Admin', value: 'Admin' },
{ label: 'Editor', value: 'Editor' },
{ label: 'Viewer', value: 'Viewer' },
] },
];
<DataTable
columns={columns}
data={rows}
editMode="row" // ← the only required change
processRowUpdate={(newRow, oldRow) => save(newRow)} // called ONCE per row, on Save
onRowEditStart={({ row }) => {}}
onRowEditStop={({ row, reason }) => {}} // reason: 'save' | 'cancel'
/>;
The Edit / Save / Cancel actions are added to the actions column automatically — no getRowActions needed.
You can also start/stop a row edit from apiRef: apiRef.current.editing.startRowEdit(rowId)
/ saveRowEdit() / cancelRowEdit() / getEditingRowId() / isRowInEditMode(rowId). Other ways to start:
double-click a cell, or focus a cell and press Enter; Enter saves the open row, Escape cancels. To
mix the edit actions with your own, spread the exported createRowEditAction(apiRef, row) into getRowActions.
Row edit mode is client-data and top-level rows only (not under virtualization or in tree sub-rows). A page change cancels the in-flight edit.
Custom cell editors
Render your own editor for a column with editComponent. It receives DataTableEditComponentProps and works
in both cell and row mode — call onCommit(value) to save (cell mode) or onChange(value) to buffer
(row mode):
import { Rating } from '@mui/material';
const columns = [
{ id: 'score', header: 'Score', accessorKey: 'score', editable: true,
editComponent: ({ value, onChange, onCommit, editMode }) => (
<Rating
value={Number(value) || 0}
onChange={(_, v) => (editMode === 'row' ? onChange(v) : onCommit(v))}
/>
) },
];
Type-aware editors
Column type | Editor |
|---|---|
text (default) | text input |
number | numeric input (commits a Number) |
date | date input (keeps Date vs string to match the source) |
select (with options) | dropdown — commits on choice |
boolean | True / False dropdown |
Props
| Prop | Type | Description |
|---|---|---|
editable (column) | boolean | (row) => boolean | Make a column's cells editable |
processRowUpdate | (newRow, oldRow) => Row | Promise<Row> | Commit hook — return the row to apply (or throw to revert) |
onProcessRowUpdateError | (error) => void | Fires when processRowUpdate throws/rejects |
In tree data only top-level rows are editable. Whole-row edit mode is client-data + top-level rows (not virtualized); a page change cancels the in-flight row edit.