Pro-ModuleGilt fürSuperScheduler Pro
Daten nach Zeitraum laden
Erzeugen Sie einen Loader mit `createRangeLoader` aus `super-scheduler/ranges`, geben Sie ihm eine Funktion `load({ start, end, signal })`, die die Ereignisse dieses halboffenen Zeitraums zurückgibt, und binden Sie ihn mit `extensions={[loader]}` an. Der Loader fordert beim Mount und nach dem Abklingen von Scrollen oder Zoomen feste Tagesabschnitte rund um den sichtbaren Zeitraum an, bricht Anfragen ab, die das Fenster verlassen, hält die zuletzt geladenen Abschnitte im Cache und führt Ereignisse nach ID zusammen. Ihr Server muss nur Abfragen `[start, end)` mit stabilen Ereignis-IDs beantworten.
Bereichsweises Laden hält eine lange Zeitleiste schnell, ohne den gesamten Datenbestand an den Browser zu schicken. Der Loader fragt Ihr Backend nach dem Zeitraum, den der Besucher sehen kann, plus einem Rand, und vergisst weit entfernte Zeiträume wieder. Er wird mit SuperScheduler Pro als super-scheduler/ranges ausgeliefert; Lite zeigt die Ereignisse an, die Sie übergeben.
Sie brauchen ihn nicht immer. Ein Plan mit einigen Tausend Ereignissen lässt sich mit einer einzigen Anfrage laden, und der Planer virtualisiert ihn (siehe Virtualisierung und Performance). Greifen Sie zum bereichsweisen Laden, wenn die Zeitleiste Jahre umfasst, wenn der vollständige Datenbestand zu groß ist, um ihn abzurufen oder im Speicher zu halten, oder wenn Ihre API ohnehin nach Datum paginiert.
Wie der Loader arbeitet
Der Loader teilt die Zeitachse in Abschnitte von chunkDays Tagen und hält ein Fenster aus Abschnitten rund um den sichtbaren Bereich:
- Feste Abschnittsgrenzen. Die Grenzen sind Vielfache von
chunkDays, gezählt ab dem 1970-01-01; sie hängen also weder vonstartDatenoch von der Scrollposition oder vonweekStartsab. Siebentägige Abschnitte beginnen an einem Donnerstag; mitchunkDays: 14undstartDate="2026-01-01"ist der erste sichtbare Abschnitt[2026-01-01, 2026-01-15). Derselbe Abschnitt erzeugt immer dieselbe Anfrage, sodass sich Antworten cachen lassen. - Anfragen an Grenzen, nie pro Frame. Das gewünschte Fenster besteht aus jedem Abschnitt, der sich mit dem sichtbaren Zeitraum überschneidet, plus
prefetchAbschnitten auf jeder Seite (ein vorab geladener Abschnitt darf vorstartDateliegen). Fehlende Abschnitte werden direkt nach dem Mount angefordert und erneut, sobald eine Scroll- oder Zoomgeste abgeklungen ist. Programmatisches Scrollen mitcontrol.scrollTo()zählt als Scrollen. - Abbruch. Eine Anfrage, deren Abschnitt das gewünschte Fenster verlässt, bevor sie beantwortet ist, wird über ihr
AbortSignalabgebrochen. Ignoriert Ihre Funktion das Signal, wird ihr verspätetes Ergebnis trotzdem verworfen. - Cache und Verdrängung. Höchstens
cacheChunksAbschnitte werden behalten. Darüber hinaus werden die Abschnitte verdrängt, die am weitesten von der Ansicht entfernt sind, und ihre Ereignisse verlassen den Planer, sofern kein anderer behaltener Abschnitt sie ebenfalls geliefert hat. Beim Zurückscrollen werden sie erneut angefordert. - Zusammenführung nach ID. Ein Ereignis, das zwei Abschnitte liefern, weil es eine Grenze überspannt, erscheint nur einmal.
- Rückmeldung. Während ein Abschnitt lädt, liegt ein durchscheinendes Band über seinem Zeitraum. Es hat die Klasse
super-scheduler__range-skeletonund verwendet das Token--super-scheduler-skeleton-base;skeleton: falseentfernt es. - Kein Verlauf. Ladevorgänge erzeugen nie Rückgängig-Einträge und erreichen
onEventsChangemitreason: 'load'.
Einen Loader anbinden
Erzeugen Sie den Loader einmal pro Planer und übergeben Sie ihn in extensions. Das Control bindet Erweiterungen anhand ihrer Objektidentität an und gibt sie wieder frei; ein Loader, der bei jedem Rendern neu erzeugt wird, würde seinen Cache also jedes Mal neu starten. Die Funktion load erhält start und end des Abschnitts als SuperScheduler.Date-Werte; start.value ist der ISO-String in bürgerlicher Zeit (2026-01-01T00:00:00), den Sie an Ihre API senden.
import { useMemo, useState, type RefObject } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { createRangeLoader } from 'super-scheduler/ranges'
import 'super-scheduler/styles.css'
import { createSaveHandler } from './save-changes'
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' },
]
/** One server row as a scheduler event. The server sends civil ISO strings with seconds. */
export function toEvent(booking: Booking): SuperScheduler.EventData {
return {
id: booking.id,
resource: booking.roomId,
start: booking.start,
end: booking.end,
text: booking.guest,
}
}
/**
* The loader is created outside render and receives the control's ref object. Its callbacks read
* `controlRef.current` later, when a chunk fails, never while React renders.
*/
function createBookingLoader(controlRef: RefObject<SuperScheduler.Scheduler | null>) {
return createRangeLoader({
// One chunk, [start, end). The signal aborts requests the visitor scrolled away from.
load: async ({ start, end, signal }) => {
const bookings = await fetchBookings(start.value, end.value, signal)
return bookings.map(toEvent)
},
chunkDays: 14,
prefetch: 1,
onError: (error, range) => {
console.error(error)
const from = range.start.toString('d MMM')
const to = range.end.addDays(-1).toString('d MMM')
controlRef.current?.message(
`Could not load ${from} to ${to}. Scroll back or refresh to retry.`,
)
},
})
}
export function RangePlanning() {
const { controlRef } = useSchedulerControl()
// Created once per scheduler: extensions are attached and disposed by object identity.
const [loader] = useState(() => createBookingLoader(controlRef))
const extensions = useMemo(() => [loader], [loader])
const onEventsChange = useMemo(() => createSaveHandler(loader), [loader])
return (
<SuperSchedulerComponent
controlRef={controlRef}
startDate="2026-01-01"
days={365}
scale="Day"
cellWidth={40}
heightSpec="Fixed"
height={480}
timeHeaders={TIME_HEADERS}
resources={ROOMS}
// No `events` prop: the loader writes into the control's own store (uncontrolled).
extensions={extensions}
onEventsChange={onEventsChange}
/>
)
}Dieser Planer hat keine events-Prop, daher schreibt der Loader direkt in den eigenen Speicher des Controls. Bearbeitungen durch den Nutzer kommen weiterhin in onEventsChange an; der folgende Handler speichert Verschiebungen und Dauer-Änderungen, überspringt Ladevorgänge und fragt den Server erneut an, wenn ein Speichern abgelehnt wird:
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerEventsChangeArgs } from 'super-scheduler'
import type { RangeLoader } from 'super-scheduler/ranges'
const ticks = (value: SuperScheduler.DateInput) => new SuperScheduler.Date(value).ticks
/** The range that covers an event before and after a change. */
function span(before: SuperScheduler.EventData, after: SuperScheduler.EventData) {
return {
start: ticks(before.start) < ticks(after.start) ? before.start : after.start,
end: ticks(before.end) > ticks(after.end) ? before.end : after.end,
}
}
/**
* Persists moves and resizes. Range loads arrive with reason 'load' and are skipped. When the server
* rejects a change, reloading the old and new dates puts the event back where the server has it.
*/
export function createSaveHandler(loader: RangeLoader) {
return ({ reason, changed, removed }: SchedulerEventsChangeArgs) => {
if (reason !== 'move' && reason !== 'resize') return
for (const after of changed) {
const before = removed.find((item) => item.id === after.id)
if (before === undefined || after.resource === undefined) continue
saveBooking({
id: String(after.id),
resource: String(after.resource),
// After a drag, start and end are SuperScheduler.Date objects; String() gives civil ISO.
start: String(after.start),
end: String(after.end),
}).catch(() => loader.reload(span(before, after)))
}
}
}Sie sollten kurz ein Platzhalterband über dem sichtbaren Zeitraum sehen, dann die Buchungen. Im Netzwerk-Panel erzeugt das Scrollen um einige Wochen nach rechts eine Anfrage pro neuem 14-Tage-Abschnitt, sobald das Scrollen stoppt, und schnelles Scrollen über viele Abschnitte hinweg erzeugt nur die Anfragen für die Stelle, an der Sie anhalten.
Optionen
| Option | Typ | Standard | Bedeutung |
|---|---|---|---|
load | ({ start, end, signal }) => Promise<EventData[]> | erforderlich | Gibt die Ereignisse zurück, die sich mit [start, end) überschneiden. |
chunkDays | number | 7 | Tage pro Abschnitt. Größere Abschnitte bedeuten weniger, aber größere Anfragen. |
prefetch | number | 1 | Abschnitte, die auf jeder Seite des sichtbaren Zeitraums geladen werden. |
cacheChunks | number | 26 | Behaltene Abschnitte, bevor die am weitesten entfernten verdrängt werden. |
skeleton | boolean | true | Zeigt das Ladeband über ausstehenden Abschnitten. |
onError | (error, { start, end }) => void | keiner | Wird einmal pro fehlgeschlagenem Abschnitt aufgerufen. Abgebrochene Anfragen sind keine Fehler. |
Das Loader-Objekt selbst stellt reload(range?), clear() und einen Getter loading bereit, der true ist, solange irgendein Abschnitt aussteht. loading ist eine einfache Eigenschaft, kein Abonnement: Lesen Sie sie, wenn Sie sie brauchen, oder steuern Sie Ihren eigenen Spinner aus load heraus.
Kontrollierte Ereignisse oder der Speicher des Controls
Bereichsweises Laden funktioniert mit beiden Datenmodi, die unter Kontrollierter Zustand beschrieben sind:
- Keine
events-Prop, oderdefaultEvents. Der Loader fügt neue Ereignisse in den Speicher des Controls ein, aktualisiert sie, wenn ein Abschnitt neu geladen wird, und entfernt sie, wenn ihr Abschnitt verdrängt wird. Ereignisse, die schon im Speicher liegen, werden von späteren Ladevorgängen nicht überschrieben; eine Buchung, die der Nutzer gerade verschoben hat, bleibt also, wo sie ist, bis Siereload()aufrufen. Das ist die einfachste Wahl, wenn Nutzer bereichsweise geladene Daten bearbeiten. - Kontrollierte
eventsplusonEventsChange. Der Loader schreibt nie in den Speicher. Jeder fertige Abschnitt ruftonEventsChangemitreason: 'load'auf, wobeieventsdie zusammengeführte Liste enthält, und Ihr State muss sie wie jede andere Änderung übernehmen. React bündelt diese Aktualisierungen; rechnen Sie mit einem Aufruf pro Abschnitt.
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SuperScheduler } from 'super-scheduler'
import { createRangeLoader } from 'super-scheduler/ranges'
import { toEvent } from './RangePlanning'
interface Props {
readonly rooms: SuperScheduler.ResourceData[]
}
export function ControlledRange({ rooms }: Props) {
// React state owns the events; the loader proposes the merged list through onEventsChange.
const [events, setEvents] = useState<SuperScheduler.EventData[]>([])
const owned = useMemo(() => events.slice(), [events])
const [loader] = useState(() =>
createRangeLoader({
load: async ({ start, end, signal }) =>
(await fetchBookings(start.value, end.value, signal)).map(toEvent),
}),
)
const extensions = useMemo(() => [loader], [loader])
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => {
// Adopt every change, loads included: `events` is the full list after this change.
setEvents([...args.events])
if (args.reason !== 'move' && args.reason !== 'resize') return
for (const after of args.changed) {
const before = args.removed.find((item) => item.id === after.id)
if (before === undefined || after.resource === undefined) continue
const change = {
id: String(after.id),
resource: String(after.resource),
start: String(after.start),
end: String(after.end),
}
saveBooking(change).then(
() => {
// The cached chunks still hold the version loaded before the change: refetch them.
loader.clear()
void loader.reload()
},
// Rejected: put the previous version back in state.
() =>
setEvents((current) => current.map((item) => (item.id === after.id ? before : item))),
)
}
},
[loader],
)
return (
<SuperSchedulerComponent
startDate="2026-01-01"
days={365}
scale="Day"
cellWidth={40}
resources={rooms}
events={owned}
onEventsChange={onEventsChange}
extensions={extensions}
/>
)
}Navigation, Aktualisierung und Filter
Zwei Methoden decken die meisten Aktionen einer Werkzeugleiste ab:
reload()fordert das sichtbare Fenster erneut an, ob im Cache oder nicht, und ersetzt diese Abschnitte. Ereignisse, die in den neuen Antworten fehlen, werden entfernt.reload({ start, end })macht dasselbe für einen beliebigen Zeitraum, was nach einem Speichern oder einer Benachrichtigung des Servers nützlich ist.clear()bricht ausstehende Anfragen ab und leert den Cache. Die Ereignisse auf dem Bildschirm entfernt es nicht.
Eine Änderung von startDate oder days über Props oder control.update() scrollt nicht von selbst und löst daher keinen Ladevorgang aus. Scrollen Sie zum neuen Zeitraum und rufen Sie reload() auf, sobald React die Änderung angewendet hat. Wenn sich die Abfrage selbst ändert, zum Beispiel ein anderer Standort oder ein Statusfilter, leeren Sie den Cache, entfernen Sie die alten Ereignisse und laden Sie neu:
import { useEffect, useMemo, useRef, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import { createRangeLoader } from 'super-scheduler/ranges'
import { toEvent } from './RangePlanning'
type SiteId = 'north' | 'south'
const ROOMS: Record<SiteId, SuperScheduler.ResourceData[]> = {
north: [
{ id: 'n1', name: 'North 1' },
{ id: 'n2', name: 'North 2' },
],
south: [
{ id: 's1', name: 'South 1' },
{ id: 's2', name: 'South 2' },
],
}
/** The loader is created once; each chunk request reads the site chosen at that moment. */
function createSiteLoader(initial: SiteId) {
let site = initial
return {
loader: createRangeLoader({
load: async ({ start, end, signal }) =>
(await fetchSiteBookings(site, start.value, end.value, signal)).map(toEvent),
}),
/** Later requests query this site. */
setSite: (next: SiteId) => {
site = next
},
}
}
export function SitePlanning() {
const { controlRef } = useSchedulerControl()
const [month, setMonth] = useState(() => SuperScheduler.Date.today().firstDayOfMonth())
const [site, setSite] = useState<SiteId>('north')
const [{ loader, setSite: setLoaderSite }] = useState(() => createSiteLoader('north'))
const extensions = useMemo(() => [loader], [loader])
// A new period is not a scroll: after React applied it, show its start and load the view.
const shown = useRef(month)
useEffect(() => {
if (shown.current.equals(month)) return
shown.current = month
controlRef.current?.scrollTo(month)
void loader.reload()
}, [controlRef, loader, month])
// Another site: forget its cache, drop its events and load the visible range again.
const changeSite = (next: SiteId) => {
setLoaderSite(next)
setSite(next)
loader.clear()
controlRef.current?.update({ events: [] })
void loader.reload()
}
return (
<>
<div role="toolbar" aria-label="Planning">
<button type="button" onClick={() => setMonth((m) => m.addMonths(-1))}>
Previous month
</button>
<button type="button" onClick={() => setMonth((m) => m.addMonths(1))}>
Next month
</button>
<button type="button" onClick={() => void loader.reload()}>
Refresh
</button>
<select value={site} onChange={(event) => changeSite(event.target.value as SiteId)}>
<option value="north">North</option>
<option value="south">South</option>
</select>
</div>
<SuperSchedulerComponent
controlRef={controlRef}
startDate={month}
days={month.daysInMonth()}
scale="Day"
cellWidth={48}
resources={ROOMS[site]}
extensions={extensions}
/>
</>
)
}Bei kontrollierten Ereignissen machen Sie dasselbe mit setEvents([]) statt mit control.update({ events: [] }) und rufen reload() aus einem Effect auf, der läuft, nachdem die leere Liste angewendet wurde. Wenn es akzeptabel ist, die Scrollposition zurückzusetzen, liefert ein Rendern des Planers mit key={site} ein frisches Control und einen frischen Loader.
Fehler und Wiederholungen
Wenn das Promise von load abgelehnt wird, ruft der Loader onError(error, { start, end }) für diesen Abschnitt auf, entfernt sein Platzhalterband und vergisst ihn. Der Abschnitt wird erneut angefordert, sobald ein abgeschlossenes Scrollen oder Zoomen ihn noch braucht, oder bei reload(). Zeigen Sie den Fehler dort, wo Nutzer hinschauen: control.message() blendet eine kurze Meldungsleiste im Planer ein, wie im ersten Beispiel. Anfragen, die der Loader abbricht, erreichen onError nie.
Der Serververtrag
Ihr Endpunkt beantwortet eine einzige Frage: Welche Ereignisse überschneiden sich mit diesem halboffenen Zeitraum in bürgerlicher Zeit?
GET /api/bookings?start=2026-01-01T00:00:00&end=2026-01-15T00:00:00[
{ "id": "b-1042", "roomId": "r101", "guest": "Ana Ruiz", "start": "2025-12-30T14:00:00", "end": "2026-01-03T11:00:00" },
{ "id": "b-1043", "roomId": "r102", "guest": "Tom Berg", "start": "2026-01-14T14:00:00", "end": "2026-01-16T11:00:00" }
]- Halboffene Überschneidung. Liefern Sie jedes Ereignis mit
event.start < endundevent.end > start. Ein Ereignis, das vor dem Abschnitt beginnt oder nach ihm endet, gehört in die Antwort, wieb-1042undb-1043oben. Ein Ereignis, das genau beistartendet, gehört nicht dazu. - Stabile, eindeutige IDs. Dieselbe Buchung muss in jeder Antwort dieselbe
idhaben, und keine zwei Ereignisse dürfen sich eine teilen, über alle Ressourcen hinweg. IDs werden strikt verglichen:1und'1'sind verschiedene Ereignisse. - Bürgerliche Zeitangaben mit Sekunden.
startundendsind Wanduhrzeiten ohne Zeitzone, und Strings brauchen Sekunden (2026-01-14T14:00:00). Wenn Sie Zeitpunkte (Instants) speichern, rechnen Sie sie zuerst in die Zeitzone des Geschäfts um; siehe Sprachen, Kalenderdaten und Zeitzonen. - Abbruch ist willkommen. Wenn Sie das
signalanfetchweitergeben, wird die Verbindung früher freigegeben; der Loader ist darauf nicht angewiesen. - Cachefähig. Feste Abschnittsgrenzen sorgen dafür, dass sich identische Anfragen wiederholen; HTTP-Caching oder ein CDN kann sie also bedienen. Halten Sie den Cache kurz, wenn andere Nutzer denselben Plan bearbeiten.
Eine Ebene tiefer: dynamicLoading und onScroll
Das Control bietet auch den klassischen, Callback-basierten Mechanismus. Mit dynamicLoading und einem onScroll-Handler ruft das Control onScroll auf, sobald das Scrollen scrollDelayDynamic Millisekunden lang geruht hat (standardmäßig 500). args.viewport enthält start, end und resources des sichtbaren Bereichs; Sie füllen args.events und rufen args.loaded() auf.
import { useCallback, useRef, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerScrollArgs, SuperScheduler } from 'super-scheduler'
import { toEvent } from './RangePlanning'
interface Props {
readonly rooms: SuperScheduler.ResourceData[]
/** Events of the first screen: onScroll is not called at mount. */
readonly initial: SuperScheduler.EventData[]
}
export function DynamicPlanning({ rooms, initial }: Props) {
const [firstScreen] = useState(() => initial.slice())
const pending = useRef<AbortController | null>(null)
// Called once scrolling has been quiet for `scrollDelayDynamic` ms.
const onScroll = useCallback((args: SchedulerScrollArgs) => {
pending.current?.abort()
const request = new AbortController()
pending.current = request
// Load a margin around the viewport: by default the result replaces every event.
const from = args.viewport.start.addDays(-14)
const to = args.viewport.end.addDays(14)
args.async = true
fetchBookings(from.value, to.value, request.signal).then(
(bookings) => {
args.events = bookings.map(toEvent)
args.loaded()
},
() => {
// Failed or superseded: keep what is on screen.
args.clearEvents = false
args.loaded()
},
)
}, [])
return (
<SuperSchedulerComponent
startDate="2026-01-01"
days={365}
scale="Day"
cellWidth={40}
resources={rooms}
defaultEvents={firstScreen}
dynamicLoading
scrollDelayDynamic={300}
onScroll={onScroll}
/>
)
}Im Vergleich zum Range-Loader bietet dieser Weg volle Kontrolle und sonst nichts: keine Ausrichtung an Abschnitten, kein Cache, kein Vorabladen, kein Platzhalterband, kein Abbruch. onScroll wird beim Mount nicht aufgerufen; rendern Sie den ersten Bildschirm also selbst. args.async ist anfangs true, sodass das Ergebnis erst angewendet wird, wenn Sie args.loaded() aufrufen. Standardmäßig ist args.clearEvents true, und die zurückgegebenen Ereignisse ersetzen alle Ereignisse; setzen Sie es auf false, um nach ID zusammenzuführen, und führen Sie zu entfernende IDs in args.remove auf. Da viewport.resources die sichtbaren Zeilen auflistet, kann dieser Mechanismus auch zeilenweise laden.
Was in Ihrer Anwendung bleibt
- Änderungen speichern. Der Loader liest; Ihr
onEventsChange-Handler oder explizite Aktionen schreiben. - Live-Aktualisierungen von anderen Nutzern. Die Optionen für automatisches Aktualisieren (
autoRefreshEnabledund verwandte) sind reserviert und bewirken nichts. Fragen Sie regelmäßig ab (Polling), wenden Sie Server-Benachrichtigungen mitcontrol.events.add/update/removean oder rufen Siereload({ start, end })für den betroffenen Zeitraum auf. - Verknüpfungen und Zeilen. Der Loader verarbeitet nur Ereignisse; übergeben Sie
linksundresourcesselbst. - Das in das Control eingebaute Laden per HTTP (
events.load(url),rows.load(url),links.load(url)) ist typisiert, aber reserviert: Es warnt in der Entwicklung und lädt nichts.
Verwandte Anleitungen: Kontrollierter Zustand, Rückgängig und Wiederholen, Ressourcen, Ereignisse und Intervalle und die API-Referenz.