Core conceptsApplies toLite and Pro
React integration, refs and lifecycle
Render SuperSchedulerComponent with the scheduler options as props. After mount, reach the control through ref.current.control, a controlRef prop, or useSchedulerControl(), which also gives it to you as state. Size it with height and heightSpec, keep object and function props stable because only props whose identity changed reach control.update(), and rely on the component to create a fresh control per mount and dispose it on unmount, which makes Strict Mode safe.
SuperSchedulerComponent is a thin React host around a DOM control, SuperScheduler.Scheduler. React renders one empty <div>; the control builds and updates everything inside it, and scrolling, zooming and dragging run without React renders. Your React code describes the configuration as props and talks to the control for imperative actions such as scrolling to a date.
This page covers the Pro component. Lite follows the same conventions with fewer options; the differences are listed at the end.
Mount the component
Every scheduler option is a prop, and every onXxx handler is a prop too:
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'
// Module constants: the same identity on every render, so they are applied once.
const ROOMS: SuperScheduler.ResourceData[] = [
{ id: 'r101', name: 'Room 101' },
{ id: 'r102', name: 'Room 102' },
{ id: 'r103', name: 'Room 103' },
]
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
{ groupBy: 'Month' },
{ groupBy: 'Day', format: 'd' },
]
export function Planning({ bookings }: { bookings: SuperScheduler.EventData[] }) {
// The control adopts the array it receives and edits it in place: give it its own copy.
const owned = useMemo(() => bookings.slice(), [bookings])
return (
// The component renders a bare <div> with no className or style props: lay it out through
// a wrapper, and style the control's root with cssClass (or classNames.root).
<section className="planning" aria-label="Room planning">
<SuperSchedulerComponent
startDate="2026-10-01"
days={31}
scale="Day"
cellWidth={44}
timeHeaders={TIME_HEADERS}
resources={ROOMS}
events={owned}
height={480}
heightSpec="Fixed"
cssClass="planning__scheduler"
/>
</section>
)
}You should see a section 480 pixels tall with a month of day columns and three rooms. The component itself accepts no className, style or id: lay it out through a wrapper element, and style the control's root element with cssClass or the classNames and styles props, as described in Theming.
The React-only props (controlRef, children, key, ref) stay in React. Every other prop is handed to the control, including names the typings do not declare, so a misspelled option is not reported by the component: rely on TypeScript to catch it.
Reach the control
The control exists only after the component mounts. There are three ways to reach it:
| Method | What you get | Use it for |
|---|---|---|
ref on the component | ref.current.control | Effects and event handlers in the same component |
controlRef prop | A ref object whose current is the control, or a callback called with it on mount | Passing the control to a parent, or to code outside React |
useSchedulerControl() | { controlRef, control }: the ref, plus the control as React state | Effects that must run when the control appears, such as creating widgets |
import { useEffect, useRef } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerEventMovedArgs } from 'super-scheduler'
const ROOMS: SuperScheduler.ResourceData[] = [{ id: 'r101', name: 'Room 101' }]
// 3. Inside handlers the control is `args.control` (and `this` in a non-arrow function).
function announceMove(args: SchedulerEventMovedArgs) {
args.control.message(`Moved to ${args.newStart.toString('d MMM')}`)
}
// 1. A ref to the component: `ref.current.control` exists after mount.
export function WithComponentRef() {
const ref = useRef<SuperSchedulerComponent>(null)
useEffect(() => {
ref.current?.control.scrollTo('2026-10-15', false, 'middle')
}, [])
return (
<SuperSchedulerComponent
ref={ref}
startDate="2026-10-01"
days={31}
scale="Day"
resources={ROOMS}
/>
)
}
// 2. useSchedulerControl(): a stable ref for handlers, plus the control as state for effects.
export function WithHook() {
const { controlRef, control } = useSchedulerControl()
useEffect(() => {
// `control` is null on the first render; the effect runs again once the scheduler mounts.
control?.scrollTo(SuperScheduler.Date.today(), 'fast', 'middle')
}, [control])
const notify = () => controlRef.current?.message('Saved', 2000)
return (
<>
<button type="button" onClick={notify}>
Notify
</button>
<SuperSchedulerComponent
controlRef={controlRef}
startDate="2026-10-01"
days={31}
scale="Day"
resources={ROOMS}
onEventMoved={announceMove}
/>
</>
)
}Some details matter in practice:
- Never read the control during render. On the first render it does not exist yet. Read it in effects, event handlers and scheduler callbacks.
useSchedulerControl()costs one extra render.controlisnullon the first render and becomes the control after mount, so effects that depend on[control]run at the right moment. ThecontrolRefit returns is stable and can be read in handlers without waiting for that render.- A
controlRefobject is cleared on unmount (set tonullwhile it still points to that control). A callbackcontrolRefis called with the control on mount and is not called withnullon unmount. - Handlers receive the control. Many handler arguments include
args.control, and in every handler written as a regularfunction,thisis the control.
To re-render React when scheduler state changes (selection, zoom, viewport, history), super-scheduler/hooks provides useScheduler({ track: [...] }), which returns { controlRef, control, state } and updates only for the topics you track, never once per animation frame.
Size the scheduler
The control fills the width of its parent. Its height is driven by two options:
heightSpec | Behavior of height |
|---|---|
'Max' (default) | The scheduler is as tall as its content, up to height pixels (default 600); beyond that it scrolls vertically |
'Fixed' | Exactly height pixels, whatever the number of rows |
'Auto' | As tall as its content, with no vertical scrollbar of its own |
'Parent100Pct' | Fills the height of the parent element |
height is the total height, time headers and horizontal scrollbar included, so no header arithmetic is needed. height="100%" is a shorthand for filling the parent. The parent must then have a definite height:
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
interface FullHeightProps {
readonly rooms: SuperScheduler.ResourceData[]
readonly bookings: SuperScheduler.EventData[]
}
export function FullHeightPlanning({ rooms, bookings }: FullHeightProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<header>Planning</header>
{/* A definite height for the scheduler to fill; minHeight 0 lets the flex item shrink. */}
<main style={{ flex: 1, minHeight: 0 }}>
<SuperSchedulerComponent
height="100%"
startDate="2026-10-01"
days={31}
scale="Day"
resources={rooms}
events={bookings}
/>
</main>
</div>
)
}control.setHeight(px) changes the height imperatively and switches to 'Fixed'. Inside SchedulerPanes, the panes component owns the height instead; see Panes and saved views.
Prop identity and memoization
On every React update the component compares each prop with its previous value using Object.is, and sends only the changed ones to control.update(). Unchanged props cost nothing. Changed props trigger a synchronous repaint of what they affect. Three consequences:
- Inline objects and arrays are "changed" on every render.
timeHeaders={[{ groupBy: 'Day' }]}orresources={rows.map(...)}written inline are sent again each time the parent renders. - Inline functions are changed on every render too. A new
onBeforeEventRenderinvalidates the rendering of every event; a newonBeforeCellRenderdrops the per-cell cache. - A prop you remove reverts to the library default. Conditionally spreading a prop in and out toggles it between your value and the default.
Keep props stable with module constants, useState, useMemo and useCallback. A practical pattern is one memoized configuration object for options and handlers, with the data passed separately:
import { useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
{ groupBy: 'Month' },
{ groupBy: 'Day', format: 'd' },
]
interface BoardProps {
readonly rooms: SuperScheduler.ResourceData[]
readonly bookings: SuperScheduler.EventData[]
/** Should be stable (useCallback in the parent): it is a dependency of the config below. */
readonly onOpen: (id: string) => void
}
export function Board({ rooms, bookings, onOpen }: BoardProps) {
const [compact, setCompact] = useState(false)
// Options and handlers in one memoized object: a parent re-render that changes none of the
// dependencies sends nothing to the control.
const config = useMemo<SchedulerProps>(
() => ({
startDate: '2026-10-01',
days: 31,
scale: 'Day',
cellWidth: compact ? 28 : 44,
density: compact ? 'compact' : 'comfortable',
timeHeaders: TIME_HEADERS,
onBeforeEventRender: (args) => {
args.data.cssClass = compact ? 'booking booking--compact' : 'booking'
},
onEventClick: (args) => onOpen(String(args.e.id())),
}),
[compact, onOpen],
)
const owned = useMemo(() => bookings.slice(), [bookings])
return (
<>
<button type="button" aria-pressed={compact} onClick={() => setCompact((value) => !value)}>
Compact
</button>
<SuperSchedulerComponent {...config} resources={rooms} events={owned} />
</>
)
}You should see the board switch between comfortable and compact density when you press the button, while unrelated parent renders send nothing to the control.
Strict Mode, unmounting and disposal
The component creates a new SuperScheduler.Scheduler in componentDidMount and calls dispose() on it in componentWillUnmount. In development, React Strict Mode mounts, unmounts and mounts again: you get a first control that is disposed immediately and a second one that stays. Nothing leaks, but your own code must follow the same discipline:
- Return a cleanup from every effect that attaches something to the control (zoom widgets, a minimap, listeners, timers). A widget created for the first, disposed control is useless and must be disposed too.
- Guard asynchronous callbacks. A request that resolves after the user left the page may find a disposed control. Check
control.disposed()before calling it: calls on a disposed control can throw. - After unmount,
ref.current.controlis the disposed control andcontrol.disposed()returnstrue. Refs created bycontrolRefanduseSchedulerControl()are reset tonull.
If your application disposes the control itself, the component notices and stops sending updates to it.
Server rendering
All entry points can be imported in Node without a DOM, so server rendering and prerendering do not crash. The server output is only the empty host <div>: the control is created in the browser when the component mounts. Reserve the space with a sized wrapper and, if the first paint matters, show a placeholder until mount. See SSR and prerendering.
Without React: the imperative host
The same control works on any element you own, for example inside another framework's component or a legacy page:
import { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'
/** Mounts a scheduler into an element you own and returns its teardown. */
export function mountPlanning(host: HTMLElement): () => void {
const control = new SuperScheduler.Scheduler(host, {
startDate: '2026-10-01',
days: 31,
scale: 'Day',
resources: [
{ id: 'r101', name: 'Room 101' },
{ id: 'r102', name: 'Room 102' },
],
events: [
{
id: 1,
resource: 'r101',
start: '2026-10-02T14:00:00',
end: '2026-10-05T11:00:00',
text: 'Booking 1042',
},
],
onEventMoved: (args) => console.info('moved', args.e.id(), args.newStart.value),
})
// Required: nothing is rendered before init(), and update() before init() throws.
control.init()
// Later changes go through update(), which repaints synchronously.
control.update({ cellWidth: 56 })
// dispose() releases the control's DOM and listeners when the host goes away.
return () => control.dispose()
}new SuperScheduler.Scheduler(elementOrId, options)accepts an element or its id.init()is required;update()beforeinit()throwsSuperScheduler.Exception.update(options)applies options and repaints synchronously.update()with no argument is a full refresh that keeps the scroll position but clears the time-range selection and keyboard focus.dispose()is your responsibility in this mode.
The package's entry also exports the React component, so React remains an installed peer dependency even when you only use the imperative host.
Lite
super-scheduler-lite exports a component with the same name and the same ref conventions: ref.current.control and a controlRef prop. Differences: there is no useSchedulerControl; a callback controlRef is called with null on unmount; height is always a fixed height; and the control has only update, scrollTo, scrollToResource, visibleStart, visibleEnd, disposed, dispose and init. See Quick start with Lite.
Next steps
- Keep events in React state: Controlled events and callbacks.
- Put React components inside events and headers: React render slots.
- Measure and tune large datasets: Performance and virtualization.