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.