Undo and Redo in React Diagram Component
21 Oct 202524 minutes to read
The React Diagram component automatically tracks all user interactions and programmatic changes, providing robust undo and redo functionality. This feature enables users to reverse or restore actions, making diagram editing more intuitive and error-tolerant.
Basic Undo and Redo Operations
The diagram provides built-in support to track changes made through both user interactions and public API calls. These changes can be reversed or restored using keyboard shortcuts or programmatic commands.
NOTE
The UndoRedo module must be injected to access undo/redo features in the diagram component.
Keyboard Shortcuts
Use these standard keyboard shortcuts for quick undo/redo operations:
-
Undo:
Ctrl+Z -
Redo:
Ctrl+Y
Programmatic undo and redo
The undo and redo methods allow you to control undo/redo operations programmatically. The following example demonstrates how to implement these methods:
// initialize Diagram component
let diagramInstance: DiagramComponent;
function App() {
return (
<DiagramComponent
id="container"
ref={(diagram) => (diagramInstance = diagram)}
width={'100%'}
height={'600px'}
/>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);
// Reverts the last action performed
diagramInstance.undo();
// Restores the last undone action
diagramInstance.redo();Enabling and Disabling Undo/Redo
Undo/Redo for diagram can be enabled/disabled with the constraints property of the diagram component.
History Change Events
The historyChange event triggers whenever an action is undone or redone, allowing you to respond to history state changes.
Advanced history management
Grouping multiple actions
Group related changes into a single undo/redo operation using the history grouping feature. This approach allows users to undo or redo multiple related changes simultaneously rather than reversing each action individually.
Use startGroupActionto begin grouping actions and endGroupAction to complete the group. The following example shows how to group multiple fill color changes:
import * as React from "react";
import * as ReactDOM from "react-dom";
import { DiagramComponent, UndoRedo, Inject } from "@syncfusion/ej2-react-diagrams";
let diagramInstance;
let nodes = [{
id: 'node',
width: 100,
height: 100,
offsetX: 300,
offsetY: 200,
annotations: [{
id: 'label1',
content: 'Rectangle'
}],
}];
function App() {
return (<DiagramComponent id="container" ref={(diagram) => (diagramInstance = diagram)} width={'100%'} height={'600px'} nodes={nodes} created={() => {
//Start to group the changes
diagramInstance.startGroupAction();
//Makes the changes
let color = ['black', 'red', 'green', 'yellow'];
for (var i = 0; i < color.length; i++) {
// Updates the fillColor for all the child elements.
diagramInstance.nodes[0].style.fill = color[i];
diagramInstance.dataBind();
}
//Ends grouping the changes
diagramInstance.endGroupAction();
}}>
<Inject services={[UndoRedo]}/>
</DiagramComponent>);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);import * as React from "react";
import * as ReactDOM from "react-dom";
import {
NodeModel,
DiagramComponent,
UndoRedo,
Inject
} from "@syncfusion/ej2-react-diagrams";
let diagramInstance: DiagramComponent;
let nodes: NodeModel[] = [{
id: 'node',
width: 100,
height: 100,
offsetX: 300,
offsetY: 200,
annotations: [{
id: 'label1',
content: 'Rectangle'
}],
}];
function App() {
return (
<DiagramComponent
id="container"
ref={(diagram) => (diagramInstance = diagram)}
width={'100%'}
height={'600px'}
nodes={nodes}
created={() => {
//Start to group the changes
diagramInstance.startGroupAction();
//Makes the changes
let color = ['black', 'red', 'green', 'yellow'];
for (var i = 0; i < color.length; i++) {
// Updates the fillColor for all the child elements.
diagramInstance.nodes[0].style.fill = color[i];
diagramInstance.dataBind();
}
//Ends grouping the changes
diagramInstance.endGroupAction();
}}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);Managing History Stack Size
The stackLimit property controls the maximum number of actions stored in the history manager. Setting an appropriate limit helps manage memory usage in applications with extensive editing operations.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { DiagramComponent, UndoRedo, Inject } from "@syncfusion/ej2-react-diagrams";
let nodes = [{
id: 'Start',
width: 100,
height: 100,
offsetX: 300,
offsetY: 200,
annotations: [{
id: 'label1',
content: 'Start'
}],
}];
function App() {
return (<DiagramComponent id="container" width={'100%'} height={'600px'} nodes={nodes} historyManager={{ stackLimit: 3 }} >
<Inject services={[UndoRedo]}/>
</DiagramComponent>);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);import * as React from "react";
import * as ReactDOM from "react-dom";
import {
DiagramComponent,
NodeModel,
UndoRedo,
Inject
} from "@syncfusion/ej2-react-diagrams";
let nodes: NodeModel[] = [{
id: 'Start',
width: 100,
height: 100,
offsetX: 300,
offsetY: 200,
annotations: [{
id: 'label1',
content: 'Start'
}],
}];
function App() {
return (
<DiagramComponent
id="container"
width={'100%'}
height={'600px'}
nodes={nodes}
historyManager={{ stackLimit: 3 }}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);Restricting History Logging
Prevent specific actions from being recorded in the history using the canLog property. This feature is useful when certain operations should not be undoable.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { DiagramComponent, UndoRedo, Inject } from "@syncfusion/ej2-react-diagrams";
let diagramInstance;
let nodes = [{
id: 'Start',
width: 100,
height: 100,
offsetX: 300,
offsetY: 200,
annotations: [{
id: 'label1',
content: 'Node'
}],
}];
function App() {
return (<DiagramComponent id="container" ref={(diagram) => (diagramInstance = diagram)} width={'100%'} height={'600px'} nodes={nodes}
created={() => {
// canLog decide whether the entry add or not in history List
diagramInstance.historyManager.canLog = function (entry) {
entry.cancel = true;
return entry;
};
}}
>
<Inject services={[UndoRedo]}/>
</DiagramComponent>);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);import * as React from "react";
import * as ReactDOM from "react-dom";
import {
DiagramComponent,
NodeModel,
UndoRedo,
Inject,
HistoryEntry
} from "@syncfusion/ej2-react-diagrams";
let diagramInstance: DiagramComponent;
let nodes: NodeModel[] = [{
id: 'Start',
width: 100,
height: 100,
offsetX: 300,
offsetY: 200,
annotations: [{
id: 'label1',
content: 'Node'
}],
}];
function App() {
return (
<DiagramComponent
id="container"
ref={(diagram) => (diagramInstance = diagram)}
width={'100%'}
height={'600px'}
nodes={nodes}
created={() => {
// canLog decide whether the entry add or not in history List
diagramInstance.historyManager.canLog = function (entry: HistoryEntry) {
entry.cancel = true;
return entry;
};
}}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);History Stack Inspection
Accessing Undo and Redo Stacks
The undoStack property is used to get the collection of undo actions which should be performed in the diagram. The redoStack property is used to get the collection of redo actions which should be performed in the diagram. The undoStack/redoStack is the read-only property.
let diagramInstance: DiagramComponent;
function App() {
return (
<DiagramComponent
id="container"
ref={(diagram) => (diagramInstance = diagram)}
width={'100%'}
height={'600px'}
nodes={nodes}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);
//get the collection of undoStack objects
let undoStack = diagramInstance.historyManager.undoStack;
//get the collection of redoStack objects
let redoStack = diagramInstance.historyManager.redoStack;Checking Availability of Undo and Redo Operations
The canUndo and canRedo properties indicate whether undo or redo operations are available. These properties return true when actions exist in their respective history stacks.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { useRef, useEffect, useState } from "react";
import { DiagramComponent, Inject, UndoRedo } from "@syncfusion/ej2-react-diagrams";
const App = () => {
const diagramRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const nodes = [
{
// Position of the node
offsetX: 250,
offsetY: 250,
// Size of the node
width: 100,
height: 100,
annotations: [
{
content: "Node",
},
],
},
];
useEffect(() => {
const diagram = diagramRef.current;
if (diagram) {
diagram.historyChange = () => {
setCanUndo(diagram.historyManager.canUndo);
setCanRedo(diagram.historyManager.canRedo);
};
}
}, []);
const handleUndo = () => {
diagramRef.current?.undo();
};
const handleRedo = () => {
diagramRef.current?.redo();
};
return (
<div>
<button onClick={handleUndo} disabled={!canUndo}>
Undo
</button>
<button onClick={handleRedo} disabled={!canRedo}>
Redo
</button>
<DiagramComponent
id="container"
ref={diagramRef}
width="100%"
height="600px"
nodes={nodes}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("diagram"));
root.render(<App />);import * as React from "react";
import * as ReactDOM from "react-dom";
import { useRef, useEffect, useState } from "react";
import { DiagramComponent, NodeModel, Inject, UndoRedo } from "@syncfusion/ej2-react-diagrams";
const App = () => {
const diagramRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const nodes: NodeModel[] = [
{
// Position of the node
offsetX: 250,
offsetY: 250,
// Size of the node
width: 100,
height: 100,
annotations: [
{
content: "Node",
},
],
},
];
useEffect(() => {
const diagram = diagramRef.current;
if (diagram) {
diagram.historyChange = () => {
setCanUndo(diagram.historyManager.canUndo);
setCanRedo(diagram.historyManager.canRedo);
};
}
}, []);
const handleUndo = () => {
diagramRef.current?.undo();
};
const handleRedo = () => {
diagramRef.current?.redo();
};
return (
<div>
<button onClick={handleUndo} disabled={!canUndo}>
Undo
</button>
<button onClick={handleRedo} disabled={!canRedo}>
Redo
</button>
<DiagramComponent
id="container"
ref={diagramRef}
width="100%"
height="600px"
nodes={nodes}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("diagram"));
root.render(<App />);Current Entry Tracking
During user interactions with nodes or connectors, the current history entry is stored in the currentEntry property of the historyManager.
The following code shows how to get the current entry from the diagram history:
import * as React from "react";
import * as ReactDOM from "react-dom";
import { DiagramComponent, UndoRedo, Inject } from "@syncfusion/ej2-react-diagrams";
let diagramInstance;
let nodes = [{
id: 'Start',
width: 100,
height: 100,
offsetX: 300,
offsetY: 100,
annotations: [{
id: 'label1',
content: 'Perform interaction with node to get current entry'
}],
}];
function App() {
const historyChange = () => {
//To get current entry
console.log(diagramInstance.historyManager.currentEntry);
};
return (<DiagramComponent id="container" ref={(diagram) => (diagramInstance = diagram)} width={'100%'} height={'600px'} nodes={nodes} historyChange={historyChange}>
<Inject services={[UndoRedo]}/>
</DiagramComponent>);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);import * as React from "react";
import * as ReactDOM from "react-dom";
import {
DiagramComponent,
NodeModel,
UndoRedo,
Inject
} from "@syncfusion/ej2-react-diagrams";
let diagramInstance: DiagramComponent;
let nodes: NodeModel[] = [{
id: 'Start',
width: 100,
height: 100,
offsetX: 300,
offsetY: 100,
annotations: [{
id: 'label1',
content: 'Perform interaction with node to get current entry'
}],
}];
function App() {
const historyChange = () => {
//To get current entry
console.log(diagramInstance.historyManager.currentEntry);
};
return (
<DiagramComponent
id="container"
ref={(diagram) => (diagramInstance = diagram)}
width={'100%'}
height={'600px'}
nodes={nodes}
historyChange={historyChange}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);History Management Utilities
Clearing History
The clearHistory method to remove all recorded actions from both undo and redo history stacks:
//Clears all the histories
diagramInstance.clearHistory();Retrieving History Stacks
The getHistoryStack method retrieves either the undoStack or redoStack from the history manager. Pass true to get the undo stack or false to get the redo stack:
// Fetch undoStack from history manager
diagramInstance.getHistoryStack(true)
// Fetch redoStack from history manager
diagramInstance.getHistoryStack(false)Event Handling
History Change Event
The historyChange event triggers whenever interactions with nodes and connectors occur. This event provides an opportunity to implement custom logic or UI updates based on history state changes:
import * as React from "react";
import * as ReactDOM from "react-dom";
import { DiagramComponent, UndoRedo, Inject } from "@syncfusion/ej2-react-diagrams";
let nodes = [{
id: 'node1',
width: 140,
height: 50,
offsetX: 300,
offsetY: 50,
annotations: [
{
content: 'Node1',
},
],
},
{
id: 'node2',
width: 140,
height: 50,
offsetX: 300,
offsetY: 140,
annotations: [
{
content: 'Node2',
},
],
}];
let connector = [
{
id: 'connector1',
sourceID: 'node1',
targetID: 'node2',
type: 'Orthogonal',
},
];
function App() {
const historyChange = (args) => {
//Triggers while interacting with diagram and performing undo-redo
console.log(args);
};
return (<DiagramComponent id="container" width={'100%'} height={'600px'} nodes={nodes} connectors={connector} historyChange={historyChange}>
<Inject services={[UndoRedo]}/>
</DiagramComponent>);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);import * as React from "react";
import * as ReactDOM from "react-dom";
import {
DiagramComponent,
NodeModel,
ConnectorModel,
UndoRedo,
Inject,
IHistoryChangeArgs
} from "@syncfusion/ej2-react-diagrams";
let nodes: NodeModel[] = [{
id: 'node1',
width: 140,
height: 50,
offsetX: 300,
offsetY: 50,
annotations: [
{
content: 'Node1',
},
],
},
{
id: 'node2',
width: 140,
height: 50,
offsetX: 300,
offsetY: 140,
annotations: [
{
content: 'Node2',
},
],
}];
let connector: ConnectorModel[] = [
{
id: 'connector1',
sourceID: 'node1',
targetID: 'node2',
type: 'Orthogonal',
},
];
function App() {
const historyChange = (args: IHistoryChangeArgs) => {
//Triggers while interacting with diagram and performing undo-redo
console.log(args);
};
return (
<DiagramComponent
id="container"
width={'100%'}
height={'600px'}
nodes={nodes}
connectors={connector}
historyChange={historyChange}
>
<Inject services={[UndoRedo]} />
</DiagramComponent>
);
}
const root = ReactDOM.createRoot(document.getElementById('diagram'));
root.render(<App />);While interacting with diagram, this event can be used to do the customization.