React Scheduler - MobX Tutorial
This tutorial shows how to render DHTMLX React Scheduler in a Vite + React + TypeScript app and drive it from a MobX store. By the end, you'll have a working Scheduler that supports create/update/delete, view + date navigation, snapshot-based undo/redo for event changes, and a read-only toggle.
The complete source code is available on GitHub.
You will build:
- a MobX store that owns
events, currentviewanddate - a
data.savebridge that converts Scheduler edits into store actions - a simple toolbar (views, navigation, undo/redo, read-only toggle) that sits above the Scheduler
Prerequisites
- Basic knowledge of React, TypeScript, Vite, and MobX
- Recommended: read Data Binding & State Management Basics to understand the data binding mode and the
data.savecallback this tutorial builds on.
Quick setup - create the project
In this step we will create a Vite project, install dependencies, and verify the app runs.
Actions:
- Create a Vite React + TypeScript project
- Install MobX + UI dependencies
- Install React Scheduler (trial package)
- Remove Vite's default
App.cssstyles so Scheduler can fill the viewport
Before you start, install Node.js.
Create a Vite React + TypeScript project:
npm create vite@latest react-scheduler-mobx-demo -- --template react-ts
cd react-scheduler-mobx-demo
Now install the required dependencies.
- For npm:
npm install mobx mobx-react-lite @mui/material @mui/icons-material @emotion/react @emotion/styled
- For yarn:
yarn add mobx mobx-react-lite @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.
Now you can start the dev server:
npm run dev
You should now have your React project running on http://localhost:5173.
To make Scheduler occupy the entire space of the page, remove the default Vite styles from src/App.css.
Update src/App.css to the following.
#root, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
Setting up sample data
In this step we will create deterministic seed data for Scheduler so the demo looks the same on every run.
Actions:
- Create
src/seed/data.tswith a small set of events - Export an initial
viewanddateso Scheduler starts in a predictable state
Create src/seed/data.ts:
export type SchedulerView = "day" | "week" | "month";
export interface SeedEvent {
id: string | number;
start_date: string;
end_date: string;
text: string;
}
export const seedEvents: SeedEvent[] = [
{ 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: SchedulerView = "week";
The companion demo includes additional events for a richer visual.
Building the control toolbar component
In this step we will build a simple reusable toolbar that controls Scheduler navigation and history.
Actions:
- Create
src/components/Toolbar.tsx - Add buttons for Day / Week / Month
- Add Prev / Today / Next navigation buttons
- Add Undo / Redo buttons wired to callbacks
- Add a Read-only toggle switch
Create src/components/Toolbar.tsx:
import { ButtonGroup, Button, Typography, Stack, FormControlLabel, Switch } from "@mui/material";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import React from "react";
export interface ToolbarProps {
currentView: string;
currentDate: Date;
isReadOnly: boolean;
canUndo?: boolean;
canRedo?: boolean;
onUndo?: () => void;
onRedo?: () => void;
onNavigate?: (action: "prev" | "next" | "today") => void;
onReadOnlyChange?: (value: boolean) => void;
setView: (view: "day" | "week" | "month") => void;
}
export default React.memo(function Toolbar({
currentView,
currentDate,
isReadOnly,
canUndo,
canRedo,
onUndo,
onRedo,
onNavigate,
onReadOnlyChange,
setView,
}: ToolbarProps) {
return (
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ m: 2 }}>
<Stack direction="row" gap={1}>
{(["day", "week", "month"] as const).map((label) => (
<Button
key={label}
variant={currentView === label ? "contained" : "outlined"}
onClick={() => setView(label)}
>
{label.charAt(0).toUpperCase() + label.slice(1)}
</Button>
))}
<ButtonGroup>
<Button onClick={() => onUndo?.()} disabled={canUndo === false}>
<UndoIcon />
</Button>
<Button onClick={() => onRedo?.()} disabled={canRedo === false}>
<RedoIcon />
</Button>
</ButtonGroup>
<FormControlLabel
label="Read-only"
control={
<Switch
checked={isReadOnly}
onChange={(e) => onReadOnlyChange?.(e.target.checked)}
inputProps={{ "aria-label": "Toggle read-only" }}
/>
}
/>
</Stack>
<Typography variant="subtitle1" sx={{ ml: 1 }}>
{new Date(currentDate).toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
})}
</Typography>
<ButtonGroup>
<Button onClick={() => onNavigate?.("prev")}> < </Button>
<Button onClick={() => onNavigate?.("today")}>Today</Button>
<Button onClick={() => onNavigate?.("next")}> > </Button>
</ButtonGroup>
</Stack>
);
});