React Scheduler - Redux Toolkit Tutorial
This tutorial shows how to connect DHTMLX React Scheduler to a Redux Toolkit store. You will keep events and UI state (view/date/config) in Redux, route Scheduler edits through data.save, and add undo/redo with snapshot-based history and a read-only toggle.
The complete source code is available on GitHub.
Prerequisites
- Node.js (LTS recommended)
- React + TypeScript basics
- Redux fundamentals (actions, reducers, store). If you need a refresher, see the Redux docs: https://redux.js.org/
Quick setup - create the project
Create a Vite + React + TS project:
npm create vite@latest scheduler-redux-demo -- --template react-ts
cd scheduler-redux-demo
npm install
Install Redux Toolkit + React Redux:
npm install @reduxjs/toolkit react-redux
Install Material UI (used for the demo toolbar):
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
Installing React Scheduler
Install React Scheduler as described in the React Scheduler installation guide.
In this tutorial we use the evaluation package:
npm install @dhtmlx/trial-react-scheduler
or
yarn add @dhtmlx/trial-react-scheduler
If you already use the Professional package, replace @dhtmlx/trial-react-scheduler with @dhx/react-scheduler in the commands and imports.
Run the dev server:
npm run dev
To make Scheduler occupy the whole page, remove the default styles from src/App.css and add:
#root {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
body {
margin: 0;
}
Configure Redux store
Create src/redux/store.ts. This wires the scheduler reducer into the Redux store:
import { configureStore } from "@reduxjs/toolkit";
import schedulerReducer from "./schedulerSlice";
export const store = configureStore({
reducer: {
scheduler: schedulerReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Define shared types
Create src/redux/types.ts. These types are shared across the slice, actions, and components:
export type SchedulerView = "day" | "week" | "month";
export interface SchedulerEvent {
id: string | number;
start_date: string;
end_date: string;
text: string;
// Scheduler may attach extra fields (e.g. custom props). Keep the demo permissive.
[key: string]: unknown;
}
export type SchedulerConfig = Record<string, unknown>;
export interface SchedulerSnapshot {
events: SchedulerEvent[];
config: SchedulerConfig;
}
SchedulerEventuses an index signature so Scheduler can attach extra fields at runtime.SchedulerSnapshotcaptures the data needed for undo/redo (events + config).
Setting up sample data
Create src/seed/data.ts with a few events and initial UI state. Notice that currentDate is stored as a number (timestamp) so the Redux state stays serializable.
export const seedEvents = [
{ id: 1, start_date: "2025-08-11T02:00:00Z", end_date: "2025-08-11T10:20:00Z", text: "Product Strategy Hike" },
{ id: 2, start_date: "2025-08-12T06:00:00Z", end_date: "2025-08-12T11:00:00Z", text: "Tranquil Tea Time" },
{ id: 3, start_date: "2025-08-15T03:00:00Z", end_date: "2025-08-15T08:00:00Z", text: "Demo and Showcase" },
];
export const seedDate = Date.parse("2025-08-15T00:00:00Z");
export const seedView = "week";
The companion demo includes additional events for a richer visual.
Define Scheduler actions (create/update/delete)
Scheduler edits will be routed through Redux actions. Create src/redux/actions.ts.
The createEvent action uses a "prepare" callback so it can generate a stable ID (simulating a backend-generated ID). We also add a small helper (dispatchAction) that returns the dispatched payload - this is useful because Scheduler's data.save can return the created/updated entity.
import { createAction } from "@reduxjs/toolkit";
import type { Dispatch } from "redux";
import type { SchedulerEvent } from "./types";
// Simulate receiving an ID from a backend.
const generateId = () => `id_${Date.now().toString()}`;
export const createEvent = createAction(
"schedulerDomain/createEvent",
(eventData: Omit<Partial<SchedulerEvent>, "id">) => {
const newEvent: SchedulerEvent = {
...(eventData as Omit<SchedulerEvent, "id">),
id: generateId(),
};
return { payload: newEvent };
}
);
export const deleteEvent = createAction(
"schedulerDomain/deleteEvent",
(id: SchedulerEvent["id"]) => ({ payload: id })
);
export const updateEvent = createAction(
"schedulerDomain/updateEvent",
(eventData: Partial<SchedulerEvent> & Pick<SchedulerEvent, "id">) => ({ payload: eventData })
);
// Helper function to dispatch an action and return its payload consistently
export function dispatchAction<Arg, Payload>(
dispatch: Dispatch,
actionCreator: (arg: Arg) => { type: string; payload: Payload },
arg: Arg
): Payload {
return dispatch(actionCreator(arg)).payload;
}
Create the Redux slice
Now create src/redux/schedulerSlice.ts. This slice stores:
events(Scheduler data)currentDate(as timestamp)view(day | week | month)config(Scheduler configuration object, includingreadonly)past/future(snapshot arrays for undo/redo)
Undo/redo is integrated directly into the slice using snapshots. Before every data-modifying action, pushHistory saves a snapshot of the current events and config. The undo and redo reducers swap the current state with a snapshot from the history.
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { seedEvents, seedDate, seedView } from "../seed/data";
import { createEvent, deleteEvent, updateEvent } from "./actions";
import type { SchedulerConfig, SchedulerEvent, SchedulerSnapshot, SchedulerView } from "./types";
interface SchedulerState {
events: SchedulerEvent[];
currentDate: number;
view: SchedulerView;
config: SchedulerConfig;
past: SchedulerSnapshot[];
future: SchedulerSnapshot[];
maxHistory: number;
}
const deepCopy = <T,>(value: T): T => {
// JSON clone is sufficient for this demo:
// - events/config are plain objects
// - we want immutable snapshots for undo/redo
return JSON.parse(JSON.stringify(value)) as T;
};
const createSnapshot = (state: SchedulerState): SchedulerSnapshot => ({
events: deepCopy(state.events),
config: deepCopy(state.config),
});
const pushHistory = (state: SchedulerState) => {
state.past.push(createSnapshot(state));
if (state.maxHistory > 0 && state.past.length > state.maxHistory) {
state.past.shift();
}
state.future = [];
};
const initialState: SchedulerState = {
events: seedEvents as unknown as SchedulerEvent[],
currentDate: seedDate,
view: seedView as SchedulerView,
config: {},
past: [],
future: [],
maxHistory: 50,
};
const schedulerSlice = createSlice({
name: "scheduler",
initialState,
reducers: {
undo(state) {
if (state.past.length === 0) return;
const previous = state.past[state.past.length - 1];
const newFuture = createSnapshot(state as SchedulerState);
state.events = previous.events;
state.config = previous.config;
state.past = state.past.slice(0, -1);
state.future = [newFuture, ...state.future];
},
redo(state) {
if (state.future.length === 0) return;
const next = state.future[0];
const newPast = createSnapshot(state as SchedulerState);
state.events = next.events;
state.config = next.config;
state.future = state.future.slice(1);
state.past = [...state.past, newPast];
},
// Navigation is not an undoable user action in this demo.
setCurrentDate(state, { payload }: PayloadAction<number>) {
state.currentDate = payload;
},
setView(state, { payload }: PayloadAction<SchedulerView>) {
state.view = payload;
},
updateConfig(state, { payload }: PayloadAction<Partial<SchedulerConfig>>) {
pushHistory(state as SchedulerState);
state.config = { ...state.config, ...payload };
},
},
extraReducers: (builder) => {
builder
.addCase(createEvent, (state, action) => {
pushHistory(state as SchedulerState);
state.events.push(action.payload);
})
.addCase(deleteEvent, (state, action) => {
pushHistory(state as SchedulerState);
state.events = state.events.filter((e) => String(e.id) !== String(action.payload));
})
.addCase(updateEvent, (state, action) => {
pushHistory(state as SchedulerState);
const index = state.events.findIndex((e) => String(e.id) === String(action.payload.id));
if (index !== -1) {
state.events[index] = { ...state.events[index], ...action.payload };
}
});
},
});
export const { undo, redo, setCurrentDate, setView, updateConfig } = schedulerSlice.actions;
export default schedulerSlice.reducer;