Skip to main content

useUndoRedo

Wraps your onCellValueChanged handler. Every event that passes through the wrapped callback is recorded; undo() replays it through your handler with oldValue and newValue swapped, redo() replays it forward. Point useInlineEdit, useFillHandle, and useCellClipboard at the wrapped callback and all of them become undoable with no extra work. The same hook powers <OGrid>'s onUndo / onRedo props.

Live demo​

Double-click to edit, then Ctrl/Cmd+Z to undo and Ctrl/Cmd+Shift+Z or Ctrl+Y to redo
Live

Quick example​

import { useHeadlessGrid, useInlineEdit, useUndoRedo } from '@alaarab/ogrid-react';
import type { ICellValueChangedEvent } from '@alaarab/ogrid-react';

// The raw writer. Must be a pure "set this cell to newValue" function.
const applyEdit = useCallback((event: ICellValueChangedEvent<Employee>) => {
setData((prev) => prev.map((row) => (row.id === event.item.id ? { ...row, [event.columnId]: event.newValue } : row)));
}, []);

const undo = useUndoRedo<Employee>({ onCellValueChanged: applyEdit, maxUndoDepth: 100 });
const commit = undo.onCellValueChanged ?? applyEdit; // defined whenever you passed a handler

const grid = useHeadlessGrid({ columns, data, getRowId });
const edit = useInlineEdit<Employee>({
columns,
getRowId,
onCellEdit: (event) => commit({ ...event, rowIndex: grid.rows.indexOf(event.item) }),
});

// Many changes, one undo step:
const onFillCells = (events: ICellValueChangedEvent<Employee>[]) => {
undo.beginBatch();
events.forEach(commit);
undo.endBatch();
};

<button onClick={undo.undo} disabled={!undo.canUndo}>Undo</button>
<button onClick={undo.redo} disabled={!undo.canRedo}>Redo</button>

Parameters​

UseUndoRedoParams<T>

ParamTypeDefaultDescription
onCellValueChanged((event: ICellValueChangedEvent<T>) => void) | undefinedrequiredYour writer. When undefined, the hook is inert and the returned callback is undefined too.
maxUndoDepthnumber100History depth. Changing it after mount rebuilds the stack and drops existing history.
formulaCellsUseUndoRedoFormulaCells<T>noneFormula-engine integration, see below.

UseUndoRedoFormulaCells<T> (optional)

FieldTypeDescription
cellOf(event) => { col; row } | nullMap a value change to an engine cell, or null if it has none.
getFormula(col, row) => string | undefinedCurrent formula at a cell.
setFormula(col, row, formula: string | null) => voidWrite or clear a formula.
onCellChanged(col, row) => voidOptional. Tell the engine a plain value changed so dependents recalculate.

Returns​

UseUndoRedoResult<T>

FieldTypeDescription
onCellValueChanged((event) => void) | undefinedThe wrapped writer. Pass this to the other hooks instead of your raw handler.
undo / redo() => voidReplay the previous / next step. No-ops when there is nothing to do.
canUndo / canRedobooleanFor button state.
beginBatch / endBatch() => voidEverything committed in between is one undo step.
clear() => voidDrop all history, for example when you replace the dataset.
maxUndoDepthnumberThe configured depth.
setFormula(col, row, formula: string | null) => voidRecord a formula change as an undoable step. No-op without formulaCells.

Behavior notes​

  • Your handler must be idempotent and direction-agnostic. Undo calls it with { ...event, oldValue: event.newValue, newValue: event.oldValue }, so the handler should simply write newValue to the cell identified by item / columnId and not, for example, append to a log.
  • The wrapped callback has a stable identity and always calls your latest handler, so it is safe to pass straight into other hooks and effects.
  • canUndo / canRedo do not update while a batch is open. They refresh when endBatch() runs. Make sure every beginBatch() is paired.
  • The wrapped callback expects a full ICellValueChangedEvent, including rowIndex. useInlineEdit emits an InlineEditEvent without it; add the index when forwarding (see the example). Fill and clipboard events already carry it.
  • Formula cells. With formulaCells, writing a plain value over a formula cell records the formula removal as part of the same step, so undo restores the formula. Formula changes made through undo.setFormula are undoable too.
  • Not persisted. The stack lives in a ref for the component's lifetime; remounting starts empty.

See also​