Getting Started with CalendarJS for React
@calendarjs/react is the official React wrapper for the CalendarJS family — Calendar, Schedule, and Timeline — shipped as a single package with idiomatic React ergonomics: camelCase callback props, controlled components, forwarded refs, and full TypeScript types.
Installation
Section titled “Installation”npm install @calendarjs/react react
First component
Section titled “First component”import React, { useState } from 'react';
import { Calendar } from '@calendarjs/react';
export default function App() {
const [date, setDate] = useState('2025-01-15');
return (
<Calendar
type="inline"
value={date}
onChange={setDate}
/>
);
}
Stylesheets are loaded automatically when you import from @calendarjs/react — any modern bundler (Vite, Webpack, Next.js, CRA) handles the import '*.css' lines inside the package. If you prefer explicit CSS:
import '@calendarjs/react/style.css';
Components
Section titled “Components”Calendar
Section titled “Calendar”Date picker with range and time selection. Keyboard navigation included.
Use for:
- Form date inputs
- Date range selection
- Time pickers
Schedule
Section titled “Schedule”Event scheduling interface with day / week / weekdays views and drag-and-drop.
Features:
- Drag and drop events
- Multiple view modes
- Resource scheduling
- Event forms
Timeline
Section titled “Timeline”Chronological event display.
Features:
- Monthly grouping
- Custom styling
- Navigation controls
- Roadmaps and logs
Idiomatic React patterns
Section titled “Idiomatic React patterns”All three components share the same React conventions:
camelCase callback props
Section titled “camelCase callback props”Write the callback name in React convention — the wrapper maps it to the underlying lemonadejs plugin:
<Schedule
onCreate={(events) => console.log('created', events)}
onChangeEvent={(event, oldValue, newValue) => console.log('updated', event)}
onDelete={(event) => console.log('deleted', event)}
/>
Your handler receives just the data — self (the plugin instance) is not passed in; you already have it via the forwarded ref. onCreate and onBeforeCreate are always called with an array of events (length ≥ 1), so you never need Array.isArray(events) ? events : [events].
Forwarded refs for imperative calls
Section titled “Forwarded refs for imperative calls”const ref = useRef();
// Later:
ref.current.next(); // Calendar / Schedule / Timeline
ref.current.setValue('2025-06-15'); // Calendar
ref.current.addEvents({ ... }); // Schedule
className and style passthrough
Section titled “className and style passthrough”<Schedule
className="my-schedule"
style={{ height: '500px' }}
/>
data-* / aria-* / role passthrough
Section titled “data-* / aria-* / role passthrough”Spread onto the wrapper <div> — useful for Playwright, React Testing Library, and screen readers:
<Schedule
data-testid="weekly-schedule"
aria-label="Class timetable"
data={events}
/>
children for overlays
Section titled “children for overlays”<Schedule ref={ref}>
{loading && <LoadingOverlay />}
</Schedule>
Controlled vs imperative
Section titled “Controlled vs imperative”Both modes are first-class. Pick by interaction style:
- Controlled — React state is the source of truth. Best for read-mostly views (timetable display, dashboards, filters).
- Imperative — talk to the instance via the ref. Best for edit-heavy interaction where a full re-render would lose focus or drag selection (drag-to-create, in-place mutations).
// Controlled — replace the array reference to update.
import { useState } from 'react';
import { Schedule } from '@calendarjs/react';
export default function App() {
const [events, setEvents] = useState([
{ start: '09:00', end: '10:00', date: '2025-01-15', title: 'Meeting' },
]);
return (
<Schedule
data={events}
onCreate={(created) => setEvents((prev) => [...prev, ...created])}
/>
);
}// Imperative — mutate via the ref, no React re-render.
import { useRef } from 'react';
import { Schedule } from '@calendarjs/react';
export default function App() {
const ref = useRef();
const flagWarning = (guid) => {
const ev = ref.current.data.find((e) => e.guid === guid);
ev.warning = true;
ref.current.render();
};
return <Schedule ref={ref} data={[/* initial events */]} />;
}Reference identity matters
Section titled “Reference identity matters”The wrapper diff-checks props with !==. Replace the array to trigger a re-render; mutating in place won't:
// ✅ New reference — re-renders
setEvents([...events, newEvent]);
// ❌ Same reference — no re-render
events.push(newEvent);
If you need to edit events in place (toggle warning, adjust color), use the imperative ref pattern above and call ref.current.render().
Memoize handlers
Section titled “Memoize handlers”The prop sync runs on every parent render. Inline arrow functions reallocate every time; the underlying !== check absorbs the no-op, but useCallback makes the intent explicit and keeps the prop diff cheap:
import { useCallback, useState } from 'react';
import { Schedule } from '@calendarjs/react';
export default function App() {
const [events, setEvents] = useState([]);
const onCreate = useCallback(
(created) => setEvents((prev) => [...prev, ...created]),
[],
);
return <Schedule data={events} onCreate={onCreate} />;
}
useCalendar, useSchedule, and useTimeline are an optional alternative to manual refs. Each returns a callback ref to pass to the component plus a reactive instance that is null before mount and the lemonadejs instance after. Useful when you want to render conditionally based on whether the component is mounted.
import { useState } from 'react';
import { Schedule, useSchedule } from '@calendarjs/react';
export default function App() {
const { ref, instance } = useSchedule();
const [events, setEvents] = useState([]);
return (
<>
<button
disabled={!instance}
onClick={() => instance.addEvents([{ start: '09:00', end: '10:00' }])}
>
Add event
</button>
<Schedule
ref={ref}
data={events}
onCreate={(created) => setEvents((prev) => [...prev, ...created])}
/>
</>
);
}
The plain useRef pattern is still fully supported — pick whichever fits your component shape.
TypeScript
Section titled “TypeScript”Full types ship with the package — no extra @types/* install needed. The package distinguishes input shapes from runtime shapes:
Schedule.EventInput/Timeline.ItemInput— loose: only the truly required field (startfor events,datefor items) is mandatory; everything else is optional. Includes an index signature so you can attach app-specific metadata (class_id,teacher_id, ...) withoutas any.Schedule.Event/Timeline.Item— strict: whatgetData()and callbacks hand back, with defaults filled in.
import { Schedule, useSchedule } from '@calendarjs/react';
import type { Schedule as ScheduleNS } from '@calendarjs/react';
import { useState } from 'react';
export default function App() {
// Loose input shape — extra fields like class_id are accepted without casts.
const [events, setEvents] = useState<ScheduleNS.EventInput[]>([
{ start: '09:00', end: '10:00', class_id: 42, teacher_id: 7 },
{ start: '10:00', end: '11:00', class_id: 42, teacher_id: 7 },
]);
// Callback receives the strict resolved shape — guid/title/color/type guaranteed.
const onCreate = (created: ScheduleNS.Event[]) => {
created.forEach((ev) => console.log(ev.guid, ev.title));
};
const { ref, instance } = useSchedule(); // ref + instance are typed automatically.
return <Schedule ref={ref} data={events} onCreate={onCreate} />;
}
Next Steps
Section titled “Next Steps”- Calendar — date picker component
- Schedule — event scheduler
- Timeline — chronological event display
- Hooks —
useCalendar/useSchedule/useTimeline - TypeScript — input vs resolved types, typed refs, app metadata
Source Code
Section titled “Source Code”MIT licensed: github.com/componade/calendarjs