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 using UndoService.add()

  • onEventMoved: record an event change using UndoService.update()

  • onEventResized: record an event change using UndoService.update()

  • onClick handler of the active area that adds a delete icon: record a removed event using UndoService.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

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 Scheduler

  • onUndo - define the event handler for the “Undo” button click

  • onRedo - 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

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;