Skip to content

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.

npm install @calendarjs/react react
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';

Date picker with range and time selection. Keyboard navigation included.

Use for:

  • Form date inputs
  • Date range selection
  • Time pickers

Calendar docs

Event scheduling interface with day / week / weekdays views and drag-and-drop.

Features:

  • Drag and drop events
  • Multiple view modes
  • Resource scheduling
  • Event forms

Schedule docs

Chronological event display.

Features:

  • Monthly grouping
  • Custom styling
  • Navigation controls
  • Roadmaps and logs

Timeline docs

All three components share the same React conventions:

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].

const ref = useRef();

// Later:
ref.current.next();                  // Calendar / Schedule / Timeline
ref.current.setValue('2025-06-15');  // Calendar
ref.current.addEvents({ ... });      // Schedule
<Schedule
    className="my-schedule"
    style={{ height: '500px' }}
/>

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}
/>
<Schedule ref={ref}>
    {loading && <LoadingOverlay />}
</Schedule>

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])}
        />
    );
}

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().

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.

Hooks reference →

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 (start for events, date for items) is mandatory; everything else is optional. Includes an index signature so you can attach app-specific metadata (class_id, teacher_id, ...) without as any.
  • Schedule.Event / Timeline.Item — strict: what getData() 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} />;
}

TypeScript guide →

  • Calendar — date picker component
  • Schedule — event scheduler
  • Timeline — chronological event display
  • Hooks — useCalendar / useSchedule / useTimeline
  • TypeScript — input vs resolved types, typed refs, app metadata

MIT licensed: github.com/componade/calendarjs