Overview
How to add undo/redo functionality to the React Scheduler component from DayPilot Pro for JavaScript.
The
UndoService
keeps a history of all drag and drop actions performed by the user. That allows unlimited undo/redo.The full history of actions and the current position is displayed below the Scheduler to .
This tutorial assumes that you are already familiar with the React Scheduler component (for an introduction, please see the React Scheduler Component Tutorial).
License
Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.
How to Use the UndoService to Track React Scheduler Changes
The undo/redo logic is implemented in the UndoService
class. The UndoService
keeps track of changes and provides an API for retrieving the last action (undo) and replaying the reverted action (redo).
First, you need to create a new instance and save it as undoService
in the React Scheduler component:
import React, { useEffect, useRef, useState } from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";
import {UndoService} from "./UndoService";
const Scheduler = () => {
const undoService = useRef(new UndoService()).current;
// ...
}
export default Scheduler;
Next, it’s necessary to initialize the UndoService
with the initial state so it can calculate changes later. When loading events, call the initialize()
method and provide the initial data set:
useEffect(() => {
const events = [
{
id: 1,
text: "Event 1",
start: "2025-09-02T00:00:00",
end: "2025-09-05T00:00:00",
resource: "A",
backColor: "#b2e0c9",
borderColor: "#8cc9a6",
fontColor: "#333"
},
// ...
];
setEvents(events);
// ...
undoService.initialize(events);
}, [undoService])
The last step is to record all changes performed by the user. The UndoService
provides add()
, update()
, and remove()
methods. Add them to the React Scheduler event handlers:
onTimeRangeSelected
: record a new event usingUndoService.add()
onEventMoved
: record an event change usingUndoService.update()
onEventResized
: record an event change usingUndoService.update()
onClick
handler of the active area that adds a delete icon: record a removed event usingUndoService.remove()
The UndoService
methods accept the changed object as the first parameter. Optionally, you can add a text description with details of the action.
const config = {
onTimeRangeSelected: async (args) => {
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
getScheduler().clearSelection();
if (modal.canceled) { return; }
const e = {
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
};
getScheduler().events.add(e);
undoService.add(e, "Event added");
},
onEventMoved: (args) => {
undoService.update(args.e.data, "Event moved");
},
onEventResized: (args) => {
undoService.update(args.e.data, "Event resized");
},
onBeforeEventRender: args => {
args.data.areas = [
{
top: 8,
right: 5,
width: 20,
height: 20,
padding: 2,
symbol: "icons/daypilot.svg#x-2",
style: "cursor: pointer",
fontColor: "#666",
toolTip: "Delete",
onClick: async args => {
const e = args.source;
undoService.remove(e.data, "Event removed");
getScheduler().events.remove(e);
}
}
];
},
// ...
};
How to Add Undo and Redo Buttons to the React Scheduler
The Buttons
React component displays the Undo and Redo buttons.
We have wrapped the undo and redo buttons in a standalone React component. It needs to use its own state - sharing the state with the Scheduler component would trigger unnecessary updates of the Scheduler.
import React, { useState, useEffect } from "react";
const Buttons = ({ service, onUndo, onRedo }) => {
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
useEffect(() => {
const updateState = () => {
setCanUndo(service.canUndo);
setCanRedo(service.canRedo);
};
service.watch(updateState);
return () => {
service.unwatch(updateState);
};
}, [service]);
const doUndo = () => {
if (typeof onUndo === "function") {
onUndo();
}
};
const doRedo = () => {
if (typeof onRedo === "function") {
onRedo();
}
};
return (
<div className={"space"}>
<button disabled={!canUndo} onClick={doUndo}>Undo</button>
<button disabled={!canRedo} onClick={doRedo}>Redo</button>
</div>
);
};
export default Buttons;
The Buttons component uses three attributes:
service
- pass the UndoService instance from the React ScheduleronUndo
- define the event handler for the “Undo” button clickonRedo
- define the event handler for the “Redo” button click
<Buttons service={undoService} onUndo={() => undo()} onRedo={() => redo()}/>
The undo()
function (defined in the Scheduler component) performs the “undo” action. It reverts the last operation retrieved from the UndoService
using UndoService.undo()
method.
const undo = () => {
const action = undoService.undo();
switch (action.type) {
case "add":
// added, need to delete now
getScheduler().events.remove(action.id);
break;
case "remove":
// removed, need to add now
getScheduler().events.add(action.previous);
break;
case "update":
// updated
getScheduler().events.update(action.previous);
break;
default:
throw new Error("Unexpected action");
}
};
The redo()
function performs the “redo” action. It gets the next action from the history (it’s the last action that was reverted using the “undo” button) and replays it:
const redo = () => {
const action = undoService.redo();
switch (action.type) {
case "add":
// added, need to re-add
getScheduler().events.add(action.current);
break;
case "remove":
// removed, need to remove again
getScheduler().events.remove(action.id);
break;
case "update":
// updated, use the new version
getScheduler().events.update(action.current);
break;
default:
throw new Error("Unexpected action");
}
};
How to Display Undo/Redo History for React Scheduler
To show the state of the history, we’ll use a special History
React component. It displays a list of changes recorded in the undo/redo history and the current position.
import React, { useState, useEffect } from "react";
const History = ({ service }) => {
const [position, setPosition] = useState(0);
const [history, setHistory] = useState([]);
useEffect(() => {
const updateState = (args) => {
setHistory(args.history);
setPosition(args.position);
};
service.watch(updateState);
return () => {
service.unwatch(updateState);
};
}, [service]);
const list = history.map((item, i) => {
const highlighted = position === i;
const highlightedClass = highlighted ? "highlighted" : "";
return (
<div key={i} className={`history-item ${highlightedClass}`}>
{item.type} - {item.text}
</div>
);
});
const total = history.length || 0;
return (
<div className={"space-20"}>
<div className={"space"} style={{ color: "gray" }}>
There are {total} items in the history:
</div>
<div className={"history-list"}>
{list}
</div>
<div className={history.length && position === history.length ? "highlighted" : ""}></div>
</div>
);
};
export default History;
To provide the History
component access to the UndoService
instance you need to pass it using the service
attribute:
<History service={undoService}/>
Full Source Code of the Scheduler Component with Undo/Redo Support
Here you can find the full JavaScript source code of the React Scheduler component with Undo/Redo support. The source code of the UndoService
class can be found in the attached project (see the top of the article).
import React, { useEffect, useRef, useState } from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";
import {UndoService} from "./UndoService";
import Buttons from "./Buttons";
import History from "./History";
const Scheduler = () => {
const schedulerRef = useRef();
const undoService = useRef(new UndoService()).current;
const [resources, setResources] = useState([]);
const [events, setEvents] = useState([]);
const config = {
timeHeaders: [
{groupBy:"Month"},
{groupBy:"Day", format:"d"}
],
scale: "Day",
days: 30,
startDate: "2025-09-01",
rowMarginTop: 2,
rowMarginBottom: 2,
durationBarVisible: false,
onBeforeEventRender: args => {
args.data.areas = [
{
top: 8,
right: 5,
width: 20,
height: 20,
padding: 2,
symbol: "icons/daypilot.svg#x-2",
style: "cursor: pointer",
fontColor: "#666",
toolTip: "Delete",
onClick: async args => {
const e = args.source;
undoService.remove(e.data, "Event removed");
getScheduler().events.remove(e);
}
}
];
},
onTimeRangeSelected: async (args) => {
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
getScheduler().clearSelection();
if (modal.canceled) { return; }
const e = {
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
};
getScheduler().events.add(e);
undoService.add(e, "Event added");
},
onEventMoved: (args) => {
undoService.update(args.e.data, "Event moved");
},
onEventResized: (args) => {
undoService.update(args.e.data, "Event resized");
},
treeEnabled: true,
};
useEffect(() => {
const events = [
{
id: 1,
text: "Event 1",
start: "2025-09-02T00:00:00",
end: "2025-09-05T00:00:00",
resource: "A",
backColor: "#b2e0c9",
borderColor: "#8cc9a6",
fontColor: "#333"
},
{
id: 2,
text: "Event 2",
start: "2025-09-03T00:00:00",
end: "2025-09-10T00:00:00",
resource: "C",
backColor: "#cdebd8",
borderColor: "#a3d4b8",
fontColor: "#333"
},
{
id: 3,
text: "Event 3",
start: "2025-09-02T00:00:00",
end: "2025-09-08T00:00:00",
resource: "D",
backColor: "#e9f6e8",
borderColor: "#c8e5c7",
fontColor: "#333"
},
{
id: 4,
text: "Event 4",
start: "2025-09-04T00:00:00",
end: "2025-09-10T00:00:00",
resource: "F",
backColor: "#f4e1d2",
borderColor: "#e2c4af",
fontColor: "#333"
}
];
setEvents(events);
const resources = [
{name: "Resource A", id: "A"},
{name: "Resource B", id: "B"},
{name: "Resource C", id: "C"},
{name: "Resource D", id: "D"},
{name: "Resource E", id: "E"},
{name: "Resource F", id: "F"},
{name: "Resource G", id: "G"}
];
setResources(resources);
undoService.initialize(events);
}, [undoService]);
const getScheduler = () => schedulerRef.current?.control;
const undo = () => {
const action = undoService.undo();
switch (action.type) {
case "add":
// added, need to delete now
getScheduler().events.remove(action.id);
break;
case "remove":
// removed, need to add now
getScheduler().events.add(action.previous);
break;
case "update":
// updated
getScheduler().events.update(action.previous);
break;
default:
throw new Error("Unexpected action");
}
};
const redo = () => {
const action = undoService.redo();
switch (action.type) {
case "add":
// added, need to re-add
getScheduler().events.add(action.current);
break;
case "remove":
// removed, need to remove again
getScheduler().events.remove(action.id);
break;
case "update":
// updated, use the new version
getScheduler().events.update(action.current);
break;
default:
throw new Error("Unexpected action");
}
};
return (
<div>
<Buttons service={undoService} onUndo={() => undo()} onRedo={() => redo()}/>
<DayPilotScheduler
{...config}
events={events}
resources={resources}
ref={schedulerRef}
/>
<h2>History</h2>
<History service={undoService}/>
</div>
);
}
export default Scheduler;