Undo and redo inline-edit commits in the React Data Grid — toolbar buttons and Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y, with the app staying the single source of truth.
history keeps an undo/redo stack of inline editing
commits. Undo with the toolbar button or Ctrl+Z
(⌘Z on macOS), redo with Ctrl+Shift+Z
or Ctrl+Y. Edit a few cells below, then undo your way
back.
import { CDataGrid } from '@coreui/react-data-grid'
import { useState } from 'react'
const roles = ['admin', 'editor', 'viewer']
const initialItems = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
age: 20 + (i % 40),
role: roles[i % roles.length]
}))
export const DataGridHistoryExample = () => {
const [items, setItems] = useState(initialItems)
return (
<CDataGrid
columns={[
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name', editable: true },
{
key: 'age', label: 'Age', width: 110, editable: { type: 'number', min: 0, max: 120 }
},
{
key: 'role', label: 'Role', width: 130, editable: { type: 'select', options: roles }
}
]}
editing
history
items={items}
itemKey={item => String(item.id)}
toolbar={{ history: true }}
onEditCommit={({ item, columnId, value }) =>
// Undo/redo re-call onEditCommit with the values swapped, so the
// handler that applies an edit also reverts it.
setItems(current =>
current.map(row => (row.id === item.id ? { ...row, [columnId]: value } : row))
)
}
/>
)
} import { CDataGrid } from '@coreui/react-data-grid'
import { useState } from 'react'
const roles = ['admin', 'editor', 'viewer']
const initialItems = Array.from({ length: 200 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
age: 20 + (i % 40),
role: roles[i % roles.length]
}))
export const DataGridHistoryExample = () => {
const [items, setItems] = useState(initialItems)
return (
<CDataGrid
columns={[
{ key: 'id', label: '#', width: 90 },
{ key: 'name', label: 'Name', editable: true },
{
key: 'age', label: 'Age', width: 110, editable: { type: 'number', min: 0, max: 120 }
},
{
key: 'role', label: 'Role', width: 130, editable: { type: 'select', options: roles }
}
]}
editing
history
items={items}
itemKey={item => String(item.id)}
toolbar={{ history: true }}
onEditCommit={({ item, columnId, value }) =>
// Undo/redo re-call onEditCommit with the values swapped, so the
// handler that applies an edit also reverts it.
setItems(current =>
current.map(row => (row.id === item.id ? { ...row, [columnId]: value } : row))
)
}
/>
)
} Usage
<CDataGrid
columns={columns}
items={items}
editing
history // track edit commits
toolbar={{ history: true }} // undo/redo buttons
onEditCommit={applyCommit}
/>The buttons stay disabled while their stack is empty; each undo/redo announces
through the ARIA live region (undoneAnnouncement/redoneAnnouncement
labels).
How undo works — the app stays in charge
The grid never mutates items. An undo re-calls onEditCommit with
value and previousValue swapped (redo re-calls the original), so the same
handler that applied the edit reverts it — no second code path:
onEditCommit={({ item, columnId, value }) =>
setItems((current) =>
current.map((row) => (row.id === item.id ? { ...row, [columnId]: value } : row))
)
}History entries reference rows by id, so they survive the immutable updates
this pattern produces. If a row disappears from the data entirely, its entries
are dropped. New commits clear the redo stack; the stack holds the last 100
edits. Following MUI and AG Grid, undo/redo covers data edits — view
changes (sorting, filters, column layout) are not tracked; persist those with
stateKey instead.