Skip to main content

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.

Loading demo…
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 / rejectonProcessRowUpdateError fires 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 asyncawait 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:

Loading demo…

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.

note

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 typeEditor
text (default)text input
numbernumeric input (commits a Number)
datedate input (keeps Date vs string to match the source)
select (with options)dropdown — commits on choice
booleanTrue / False dropdown

Props

PropTypeDescription
editable (column)boolean | (row) => booleanMake a column's cells editable
processRowUpdate(newRow, oldRow) => Row | Promise<Row>Commit hook — return the row to apply (or throw to revert)
onProcessRowUpdateError(error) => voidFires when processRowUpdate throws/rejects
note

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.