| title | React Scheduler |
|---|---|
| sidebar_label | Overview |
| description | Overview of the React Scheduler wrapper, data binding modes, customization options, and framework compatibility. |
:::note React Scheduler is available under Commercial, Enterprise and Ultimate licenses. If you're using Individual or GPL editions of Scheduler, use dhtmlxScheduler with React. :::
DHTMLX React Scheduler is the official React wrapper for DHTMLX Scheduler. It provides a declarative API for rendering and configuring Scheduler while still exposing the underlying Scheduler instance when you need advanced control.
Key features:
- pass
events,view, anddateas props - customize behavior with
configandtemplates - handle user changes through
data.saveordata.batchSave - use
refto access Scheduler API methods directly
If you're new to DHTMLX Scheduler, see the DHTMLX Scheduler documentation for an overview of its features.
For evaluation and professional package installation, see:
- React
18+
import { useMemo, useRef } from "react";
import ReactScheduler, {
type Event,
type ReactSchedulerRef,
type SchedulerConfig,
type SchedulerTemplates,
} from "@dhtmlx/trial-react-scheduler";
import "@dhtmlx/trial-react-scheduler/dist/react-scheduler.css";
const events: Event[] = [
{
id: 1,
text: "Product Strategy Hike",
classname: "blue",
start_date: new Date("2025-12-08T02:00:00Z"),
end_date: new Date("2025-12-08T10:20:00Z"),
},
];
export default function BasicSchedulerDemo() {
const schedulerRef = useRef<ReactSchedulerRef>(null);
const templates: SchedulerTemplates = useMemo(
() => ({
event_class: (_start, _end, event) => event.classname || "",
}),
[]
);
const config: SchedulerConfig = useMemo(
() => ({
first_hour: 6,
last_hour: 22,
hour_size_px: 60,
}),
[]
);
return (
<div style={{ height: "100vh" }}>
<ReactScheduler
ref={schedulerRef}
events={events}
view="week"
date={new Date("2025-12-08T00:00:00Z")}
templates={templates}
config={config}
/>
</div>
);
}React Scheduler supports two data-binding models.
In this model, React (or a state manager) owns event data:
- Scheduler reads events from props
- user changes call your
data.savecallback - callback updates React state
- React re-renders Scheduler with updated props
import { useMemo, useState } from "react";
import ReactScheduler, { type Event } from "@dhtmlx/trial-react-scheduler";
export default function ReactDrivenExample({ seedEvents }: { seedEvents: Event[] }) {
const [events, setEvents] = useState<Event[]>(seedEvents);
const data = useMemo(
() => ({
save: (entity: string, action: string, item: Event, id: string | number) => {
if (entity !== "event") return;
if (action === "create") {
setEvents((prev) => [...prev, item]);
return;
}
if (action === "update") {
setEvents((prev) => prev.map((e) => (e.id === id ? item : e)));
return;
}
if (action === "delete") {
setEvents((prev) => prev.filter((e) => e.id !== id));
}
},
}),
[]
);
return <ReactScheduler events={events} data={data} />;
}This model is best when other React UI must stay synchronized with Scheduler data.
In this model, Scheduler manages its internal state and forwards edits to your backend.
<ReactScheduler
data={{
load: "/api/scheduler/load",
save: "/api/scheduler/save",
}}
/>This model is useful when React does not need to mirror every update immediately.
You can load data using either props or data.load:
// Props-based loading
<ReactScheduler events={eventsFromState} />
// Transport-based loading
<ReactScheduler data={{ load: "/api/scheduler/load" }} />For data format requirements, see Loading Data.
data.save can be a URL or a callback.
<ReactScheduler
data={{
save: async (entity, action, item, id) => {
if (entity !== "event") return;
if (action === "create") {
const response = await fetch("/api/events", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(item),
});
const created = await response.json();
return { id: created.id };
}
if (action === "update") {
await fetch(`/api/events/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(item),
});
}
if (action === "delete") {
await fetch(`/api/events/${id}`, { method: "DELETE" });
}
},
}}
/>For backend behavior details, see Server Integration.
Scheduler includes a built-in event editor called Lightbox.
You can replace it by using customLightbox:
import React, { useState } from 'react';
export interface CustomLightboxProps {
data?: any;
onSave?: (event: any) => void;
onCancel?: () => void;
onDelete?: () => void;
}
const CustomLightbox: React.FC<CustomLightboxProps> = ({
data,
onSave,
onCancel,
onDelete
}) => {
let updatedEventText = data.text || "";
const handleSaveClick = () => {
if(onSave)
onSave({ ...data, text: updatedEventText });
};
function PaperComponent(props: any) {
const nodeRef = React.useRef(null);
return (
<Draggable
nodeRef={nodeRef"
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"], input,textarea'}
>
<Paper {...props} ref={nodeRef}/>
</Draggable>
);
}
function TextComponent() {
const [description, setDescription] = useState<string>(data.text || '');
return (
<TextField
id="event_text"
hiddenLabel
multiline
value="{description}"
autoFocus
onChange="{(e)" => {
updatedEventText = e.target.value;
setDescription(e.target.value)
}}
sx="{{" width: '100%', padding: '8px', marginTop: '10px' }}
/>
)
}
return (
<Dialog
open={true}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog-title"
className="lightbox"
onClose={onCancel}
>
<DialogTitle style={{ cursor: 'move' }} id="draggable-dialog-title">
Edit Event
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Description
</DialogContentText>
<TextComponent />
<DialogActions className='buttons'>
<Button variant="contained" onClick={handleSaveClick}>Save</Button>
<Button variant="contained" onClick={onCancel}>Cancel</Button>
<Button variant="contained" onClick={onDelete}>Delete</Button>
</DialogActions>
</DialogContent>
</Dialog>
);
};
export default CustomLightbox;You can also intercept the editor opening with onBeforeLightbox:
import { useEffect, useRef } from 'react';
import ReactScheduler from "@dhx/react-scheduler";
import "@dhx/react-scheduler/dist/react-scheduler.css";
import { useNavigate } from 'react-router-dom';
export default function BasicInitDemo() {
const schedulerRef = useRef<any>(null);
const { events, handleSaveEvent, handleDeleteEvent, createEvent }
= useOutletContext<SchedulerEditorContext>();
const navigate = useNavigate();
const handleEventEdit = (id: any) => {
const schedulerInstance = schedulerRef.current?.instance;
navigate(`/editor/${id}`, { state: { task: schedulerInstance.getTask(id) } });
};
return (
<ReactScheduler
ref={schedulerRef}
tasks={events}
onBeforeLightbox={handleEventEdit} />
);
}Reference: onBeforeLightbox
The delete confirmation dialog can be overridden via modals.
<ReactScheduler
modals={{
onBeforeEventDelete: ({ event, callback, schedulerInstance }) => {
if (window.confirm(`Delete "${event.text}"?`)) {
callback(); // calling the callback will delete the event
}
},
}}
/>When a user edits or drags a recurring event, a confirmation modal asks whether to modify just this occurrence, this and following events, or the entire series. You can replace this built-in dialog with your own using modals.onRecurrenceConfirm.
The callback receives a context object and must return a decision (or a Promise that resolves to one):
| Field | Type | Description |
|---|---|---|
origin |
"lightbox" | "dnd" |
Whether the action was triggered from the lightbox or drag-and-drop |
occurrence |
any |
The specific occurrence being edited |
series |
any |
The parent recurring event |
labels |
object |
Localized labels: title, ok, cancel, occurrence, following, series |
options |
string[] |
Available choices, e.g. ["occurrence", "following", "series"] |
Return value (RecurrenceDecision): "occurrence", "following", "series", or null to cancel.
Example:
import { useState, useCallback } from "react";
function App() {
const [recurrencePrompt, setRecurrencePrompt] = useState(null);
const onRecurrenceConfirm = useCallback((context) => {
return new Promise((resolve) => {
setRecurrencePrompt({ context, resolve });
});
}, []);
return (
<>
<ReactScheduler
modals={{ onRecurrenceConfirm }}
/>
{recurrencePrompt && (
<MyRecurrenceDialog
options={recurrencePrompt.context.options}
labels={recurrencePrompt.context.labels}
onSelect={(choice) => {
recurrencePrompt.resolve(choice);
setRecurrencePrompt(null);
}}
onCancel={() => {
recurrencePrompt.resolve(null);
setRecurrencePrompt(null);
}}
/>
)}
</>
);
}Use the filter prop to control which events are displayed:
import { useCallback, useState } from "react";
function FilteredScheduler({ events }: { events: any[] }) {
const [query, setQuery] = useState("");
const filterFn = useCallback(
(event: any) => {
if (!query.trim()) return true;
return event.text?.toLowerCase().includes(query.trim().toLowerCase());
},
[query]
);
return (
<>
<input
placeholder="Search events..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ReactScheduler events={events} filter={filterFn} />
</>
);
}When props are not enough, access the Scheduler instance through ref:
import { useEffect, useRef } from "react";
import ReactScheduler, { type ReactSchedulerRef } from "@dhx/react-scheduler";
export function DirectRefExample({ events }: { events: any[] }) {
const schedulerRef = useRef<ReactSchedulerRef>(null);
useEffect(() => {
const scheduler = schedulerRef.current?.instance;
if (!scheduler) return;
console.log("Events:", scheduler.getEvents());
scheduler.setCurrentView(new Date());
}, []);
return <ReactScheduler ref={schedulerRef} events={events} />;
}If you mutate Scheduler directly, keep React props synchronized to avoid state drift.
See Scheduler Methods Overview for available methods.
- If you manually call
scheduler.parse(( events ))orscheduler.addEvent()from your code, be aware you may need to keep the React props in sync. Otherwise, the next time React re-renders, it may overwrite your manual changes. - The recommended approach is to rely on the wrapper's props for events, or manage them in your React state. Then let the wrapper handle re-parsing.
:::note React Scheduler is SSR-friendly. During server rendering, it outputs a placeholder container and hydrates on the client. :::
Use framework-specific guides for details: