Choose an edit transaction model
Immediate saves need optimistic UI, request cancellation, server rejection handling, and row-version conflicts. Staged saves need dirty indicators, batch validation, review, partial failure behavior, and discard or rollback.
Validate at three layers
Use parsers and column validators for immediate feedback, Pro validation workflows for richer grid rules, and backend validation for authority. Never rely on the browser to enforce permissions or business invariants.
Protect identity and recovery
Use stable row IDs, include a version or ETag in writes, keep the failed user value visible, explain server errors at the affected cell or row, and provide a retry or reset path. Test concurrent edits with two sessions.
Handle concurrent changes
Include a row version or ETag in writes. When another user changes the same record, show which value is stale and let the user refresh, retry, or reconcile. Test a row moving out of the current filter after an edit, permissions changing during a session, and partial batch success. Stable row IDs are essential for attaching outcomes to the correct record.
Design permissions and protected fields
The server determines which users can edit each record and field. The grid can disable or hide editors for clarity, but client state is not enforcement. Recheck permission at commit time and return a specific outcome. Test permission changes during an open session and avoid exposing protected values through exports, clipboard, or validation messages.
Test bulk editing and failure recovery
Paste, fill, multi-row actions, and batch saves can affect many records. Define atomic versus partial behavior, validation summaries, progress, cancellation, retry, and rollback. Return outcomes keyed by stable row and column identity. Users should be able to correct rejected values without re-entering successful changes.
Return server outcomes by row and column
For batch edits, the backend should return outcomes keyed by stable row ID and column key. That lets the grid show exactly which cells saved, failed validation, hit a permission issue, or conflicted with a newer version. Without precise outcomes, users lose trust because one failed value can make the entire grid feel uncertain.
Use one concrete save flow
For example, a user edits ARR, the grid marks the row dirty, local validation checks the value, and the server receives row ID, column key, new value, and version. The server then returns saved, rejected, or conflicted. This flow is more meaningful than simply saying cells are editable.
Show failed edits without losing context
When a save fails, users should still see the attempted value, the previous saved value when needed, and the reason for failure. Clearing the editor or replacing the row without explanation creates distrust. This is especially important for finance, operations, and compliance workflows where edits carry business consequences.
Test the keyboard edit lifecycle
Test Tab, Shift+Tab, Enter, Escape, arrows, edit entry, cancellation, commit, validation failure, disabled cells, custom controls, and movement at pinned or virtualized boundaries. Confirm the active row and column remain clear to assistive technology. Custom editors must return focus predictably and should not trap users inside the cell.
Controlled cell updates with pending row state
import { useState } from "react";
import { Grid, type CellValue, type GridRow } from "@ace-grid/core";
export function EditableOrdersGrid({ initialRows, columns }) {
const [rows, setRows] = useState<GridRow[]>(initialRows);
const [pending, setPending] = useState(new Set<string>());
const updateCell = (
rowId: string | number,
columnKey: string,
value: CellValue,
) => {
setRows((current) =>
current.map((row) =>
row.id === rowId
? { ...row, data: { ...row.data, [columnKey]: value } }
: row,
),
);
setPending((current) => new Set(current).add(String(rowId)));
};
return (
<Grid
data={{ rows, columns }}
columns={{ columnWidths: {}, fillWidth: true }}
layout={{ width: 1200, height: 520 }}
edit={{ isCellEditing: true, onCellChange: updateCell }}
virtual={{ enableVirtualization: true }}
/>
);
}