Inline Editing
Inline editing is opt-in per column. The table owns the edit lifecycle, focus movement, pending state, validation display, and stale commit protection. The host application owns the editor markup and data mutation.
Basic Editor
Add editable, getEditValue, and renderEditCell to a column:
const columns: Array<DataTableColumn<Account>> = [
{
id: "owner",
header: "Owner",
editable: true,
getEditValue: (row) => row.owner,
renderCell: (row) => row.owner,
renderEditCell: ({ value, setValue, commit, cancel }) => (
<form aria-label="Edit owner">
<input
aria-label="Owner"
value={String(value ?? "")}
onChange={(event) => setValue(event.target.value)}
/>
<button type="button" onClick={() => commit()}>Save</button>
<button type="button" onClick={cancel}>Cancel</button>
</form>
)
}
];Persist changes in onCellEditCommit:
<DataTable
rows={rows}
columns={columns}
getRowId={(row) => row.id}
onCellEditCommit={({ rowId, column, value }) => {
if (column.id === "owner") {
updateAccountOwner(rowId, String(value ?? ""));
}
}}
/>The table does not mutate rows. Update your local store, server cache, or query result after validation succeeds.
The edit lifecycle types are exported for shared column factories and server adapters:
DataTableCellEditdescribes the currently edited{ rowId, columnId }.DataTableEditCellContext<T>is passed torenderEditCell.DataTableCellEditCommit<T>is passed toonCellEditCommit.DataTableCellEditCommitResultis the optional commit return value for validation and close control.
Async Validation and Commit
Commit handlers can return a promise. While the promise is pending, pending is true, duplicate commits are ignored, and Escape cancel is blocked.
<DataTable
rows={rows}
columns={columns}
getRowId={(row) => row.id}
onCellEditCommit={async ({ rowId, value }) => {
const owner = String(value ?? "").trim();
if (!owner) {
return { close: false, error: "Owner is required" };
}
await saveOwner(rowId, owner);
}}
/>Inside renderEditCell, use pending, error, and errorId:
renderEditCell: ({ value, setValue, commit, cancel, pending, error, errorId }) => (
<form aria-label="Edit owner">
<input
aria-label="Owner"
aria-describedby={error ? errorId : undefined}
aria-invalid={error ? "true" : "false"}
disabled={pending}
value={String(value ?? "")}
onChange={(event) => setValue(event.target.value)}
/>
<button type="button" disabled={pending} onClick={() => commit()}>
{pending ? "Saving" : "Save"}
</button>
<button type="button" disabled={pending} onClick={cancel}>
Cancel
</button>
</form>
)Thrown or rejected errors keep the editor open and render the message as a package alert. Returning { close: false, error } does the same without throwing.
Simple Select Editors
Editors do not need a popover when the value is compact. Use native controls for enum values:
renderEditCell: ({ value, setValue, commit, cancel }) => (
<form aria-label="Edit status">
<select
aria-label="Status"
value={String(value ?? "active")}
onChange={(event) => setValue(event.target.value)}
>
<option value="active">Active</option>
<option value="review">In review</option>
<option value="blocked">Blocked</option>
</select>
<button type="button" onClick={() => commit()}>Save</button>
<button type="button" onClick={cancel}>Cancel</button>
</form>
)Controlled Editing
Use editingCell with onEditingCellChange when the host controls which cell is open:
const [editingCell, setEditingCell] = useState<DataTableCellEdit | undefined>();
<DataTable
rows={rows}
columns={columns}
getRowId={(row) => row.id}
editingCell={editingCell}
onEditingCellChange={setEditingCell}
/>Passing editingCell={undefined} keeps editing controlled in a closed state. Edit triggers call onEditingCellChange, but the editor does not open until the host supplies the requested cell.
Stale Edit Protection
The table clears editing state when the edited row or column leaves the visible model because of column visibility, filtering, grouping collapse, or editability changes. Stale async commits from a removed or replaced editor are ignored.
While a controlled editor is open, refreshed rows can update the displayed value until the user changes the draft. Once the draft is dirty, later refreshes do not overwrite the user's in-progress edit.
Keyboard Behavior
- Opening an editor focuses the first focusable control inside the editor.
- Escape cancels editing.
- Ctrl+Enter or Cmd+Enter commits the current value.
commit()commits the current draft.commit(value)commits the provided value, including explicitundefined.
Custom editors may add their own field-specific shortcuts, but should keep labelled controls and clear save/cancel affordances.