Zum Inhalt springen
SuperScheduler

GrundbegriffeGilt fürLite und Pro

React-Integration, Refs und Lebenszyklus

Rendern Sie SuperSchedulerComponent mit den Optionen des Planers als Props. Nach dem Mounten erreichen Sie das Control über ref.current.control, eine Prop controlRef oder useSchedulerControl(), das es Ihnen zusätzlich als State liefert. Die Größe legen Sie mit height und heightSpec fest. Halten Sie Objekt- und Funktions-Props stabil, denn nur Props mit geänderter Identität erreichen control.update(), und verlassen Sie sich darauf, dass die Komponente pro Mount ein neues Control erzeugt und es beim Unmounten freigibt; das macht Strict Mode sicher.

Geprüft mit v0.1.0 · überarbeitet am 7. Oktober 2026.md

SuperSchedulerComponent ist ein schlanker React-Host um ein DOM-Control, SuperScheduler.Scheduler. React rendert ein einziges leeres <div>; das Control baut und aktualisiert alles darin, und Scrollen, Zoomen und Ziehen laufen ohne React-Renderings. Ihr React-Code beschreibt die Konfiguration als Props und spricht für imperative Aktionen, etwa das Scrollen zu einem Datum, mit dem Control.

Diese Seite behandelt die Pro-Komponente. Lite folgt denselben Konventionen mit weniger Optionen; die Unterschiede stehen am Ende.

Die Komponente mounten

Jede Option des Planers ist eine Prop, und jeder onXxx-Handler ebenfalls:

src/Planning.tsxtsx
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>
  )
}

Sie sollten einen 480 Pixel hohen Abschnitt mit einem Monat an Tagesspalten und drei Zimmern sehen. Die Komponente selbst akzeptiert weder className noch style noch id: Positionieren Sie sie über ein umschließendes Element, und gestalten Sie das Wurzelelement des Controls mit cssClass oder den Props classNames und styles, wie in Themes beschrieben.

Die reinen React-Props (controlRef, children, key, ref) bleiben in React. Jede andere Prop wird an das Control übergeben, auch Namen, die die Typdefinitionen nicht deklarieren; eine falsch geschriebene Option meldet die Komponente also nicht. Verlassen Sie sich darauf, dass TypeScript sie findet.

Das Control erreichen

Das Control existiert erst, nachdem die Komponente gemountet ist. Es gibt drei Wege dorthin:

MethodeWas Sie erhaltenVerwenden Sie sie für
ref auf der Komponenteref.current.controlEffects und Event-Handler in derselben Komponente
Prop controlRefEin Ref-Objekt, dessen current das Control ist, oder einen Callback, der beim Mounten damit aufgerufen wirdDie Übergabe des Controls an eine Elternkomponente oder an Code außerhalb von React
useSchedulerControl(){ controlRef, control }: das Ref und dazu das Control als React-StateEffects, die laufen müssen, sobald das Control erscheint, etwa zum Erzeugen von Widgets
src/ControlAccess.tsxtsx
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}
      />
    </>
  )
}

In der Praxis kommt es auf einige Details an:

  • Lesen Sie das Control nie während des Renderns. Beim ersten Rendering existiert es noch nicht. Lesen Sie es in Effects, Event-Handlern und Callbacks des Planers.
  • useSchedulerControl() kostet ein zusätzliches Rendering. control ist beim ersten Rendering null und wird nach dem Mounten zum Control, sodass Effects, die von [control] abhängen, im richtigen Moment laufen. Das zurückgegebene controlRef ist stabil und lässt sich in Handlern lesen, ohne auf dieses Rendering zu warten.
  • Ein controlRef-Objekt wird beim Unmounten geleert (auf null gesetzt, solange es noch auf dieses Control zeigt). Ein controlRef-Callback wird beim Mounten mit dem Control aufgerufen und beim Unmounten nicht mit null.
  • Handler erhalten das Control. Viele Handler-Argumente enthalten args.control, und in jedem Handler, der als normale function geschrieben ist, ist this das Control.

Damit React neu rendert, wenn sich der Zustand des Planers ändert (Auswahl, Zoom, Viewport, Verlauf), bietet super-scheduler/hooks den Hook useScheduler({ track: [...] }). Er gibt { controlRef, control, state } zurück und aktualisiert nur für die Themen, die Sie verfolgen, nie einmal pro Animationsframe.

Die Größe des Planers festlegen

Das Control füllt die Breite seines Elternelements. Seine Höhe steuern zwei Optionen:

heightSpecVerhalten von height
'Max' (Standard)Der Planer ist so hoch wie sein Inhalt, höchstens height Pixel (Standard 600); darüber hinaus scrollt er vertikal
'Fixed'Genau height Pixel, unabhängig von der Anzahl der Zeilen
'Auto'So hoch wie sein Inhalt, ohne eigene vertikale Scrollleiste
'Parent100Pct'Füllt die Höhe des Elternelements

height ist die Gesamthöhe, Zeitköpfe und horizontale Scrollleiste eingeschlossen; Sie müssen also nichts für die Köpfe herausrechnen. height="100%" ist eine Kurzform, um das Elternelement zu füllen. Das Elternelement braucht dann eine bestimmte Höhe:

src/FullHeightPlanning.tsxtsx
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) ändert die Höhe imperativ und schaltet auf 'Fixed' um. Innerhalb von SchedulerPanes verwaltet stattdessen die Bereichskomponente die Höhe; siehe Bereiche und gespeicherte Ansichten.

Identität von Props und Memoisierung

Bei jedem React-Update vergleicht die Komponente jede Prop mit Object.is mit ihrem vorherigen Wert und gibt nur die geänderten an control.update() weiter. Unveränderte Props kosten nichts. Geänderte Props lösen ein synchrones Neuzeichnen dessen aus, was sie betreffen. Drei Folgen:

  • Inline-Objekte und -Arrays gelten bei jedem Rendering als „geändert“. Inline geschriebene timeHeaders={[{ groupBy: 'Day' }]} oder resources={rows.map(...)} werden bei jedem Rendering der Elternkomponente erneut gesendet.
  • Auch Inline-Funktionen ändern sich bei jedem Rendering. Ein neues onBeforeEventRender macht das Rendering aller Ereignisse ungültig; ein neues onBeforeCellRender verwirft den Cache pro Zelle.
  • Eine Prop, die Sie entfernen, fällt auf den Standardwert der Bibliothek zurück. Wird eine Prop per Spread bedingt mal übergeben und mal nicht, wechselt sie zwischen Ihrem Wert und dem Standard hin und her.

Halten Sie Props mit Modulkonstanten, useState, useMemo und useCallback stabil. Bewährt hat sich ein memoisiertes Konfigurationsobjekt für Optionen und Handler, während die Daten separat übergeben werden:

src/Board.tsxtsx
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} />
    </>
  )
}

Sie sollten sehen, wie die Tafel beim Drücken des Buttons zwischen komfortabler und kompakter Dichte wechselt, während Renderings der Elternkomponente ohne Bezug dazu nichts an das Control senden.

Strict Mode, Unmounten und Freigeben

Die Komponente erzeugt in componentDidMount ein neues SuperScheduler.Scheduler und ruft in componentWillUnmount dessen dispose() auf. In der Entwicklung mountet React Strict Mode, unmountet und mountet erneut: Sie erhalten ein erstes Control, das sofort freigegeben wird, und ein zweites, das bleibt. Nichts leckt, aber Ihr eigener Code muss derselben Disziplin folgen:

  • Geben Sie aus jedem Effect, der etwas an das Control hängt, eine Cleanup-Funktion zurück (Zoom-Widgets, eine Minimap, Listener, Timer). Ein Widget, das für das erste, bereits freigegebene Control erzeugt wurde, ist nutzlos und muss ebenfalls freigegeben werden.
  • Sichern Sie asynchrone Callbacks ab. Eine Anfrage, die erst antwortet, nachdem der Nutzer die Seite verlassen hat, trifft womöglich auf ein freigegebenes Control. Prüfen Sie control.disposed(), bevor Sie es aufrufen: Aufrufe auf einem freigegebenen Control können einen Fehler werfen.
  • Nach dem Unmounten ist ref.current.control das freigegebene Control, und control.disposed() gibt true zurück. Refs, die über controlRef und useSchedulerControl() entstanden sind, werden auf null zurückgesetzt.

Gibt Ihre Anwendung das Control selbst frei, bemerkt die Komponente das und sendet ihm keine Updates mehr.

Server-Rendering

Alle Einstiegspunkte lassen sich in Node ohne DOM importieren; Server-Rendering und Prerendering stürzen also nicht ab. Die Ausgabe des Servers ist nur das leere Host-<div>: Das Control wird im Browser erzeugt, wenn die Komponente gemountet wird. Reservieren Sie den Platz mit einem umschließenden Element fester Größe und zeigen Sie, falls der erste Paint zählt, bis zum Mounten einen Platzhalter. Siehe SSR und Prerendering.

Ohne React: der imperative Host

Dasselbe Control funktioniert auf jedem Element, das Ihnen gehört, etwa in der Komponente eines anderen Frameworks oder auf einer Legacy-Seite:

src/mount-planning.tsts
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) akzeptiert ein Element oder dessen ID.
  • init() ist Pflicht; update() vor init() wirft eine SuperScheduler.Exception.
  • update(options) wendet Optionen an und zeichnet synchron neu. update() ohne Argument ist eine vollständige Aktualisierung, die die Scrollposition behält, aber die Auswahl von Zeiträumen und den Tastaturfokus aufhebt.
  • dispose() liegt in diesem Modus in Ihrer Verantwortung.

Der Einstiegspunkt des Pakets exportiert auch die React-Komponente; React bleibt also eine installierte Peer-Abhängigkeit, selbst wenn Sie nur den imperativen Host verwenden.

Lite

super-scheduler-lite exportiert eine Komponente mit demselben Namen und denselben Ref-Konventionen: ref.current.control und eine Prop controlRef. Die Unterschiede: Es gibt kein useSchedulerControl; ein controlRef-Callback wird beim Unmounten mit null aufgerufen; height ist immer eine feste Höhe; und das Control hat nur update, scrollTo, scrollToResource, visibleStart, visibleEnd, disposed, dispose und init. Siehe Schnellstart mit Lite.

Planung einer VideoproduktionEin Dreh dauert länger. Verschieben Sie den abhängigen Schnitt, verstehen Sie warum, und nehmen Sie es zurück.

Nächste Schritte