# Kontrollierte Ereignisse und Callbacks

> Ereignisse mit onEventsChange im React-State halten, dem Control eine Kopie geben, die Reihenfolge der Callbacks kennen und optimistisch oder bestätigt speichern.

Source: https://superscheduler.org/de/docs/controlled-state/
Reviewed: 2026-10-07

Übergeben Sie die Ereignisse aus dem React-State und schreiben Sie sie in onEventsChange zurück. Das Control ruft diesen Callback einmal pro Task nach einem Ablegen, einer Dauer-Änderung oder einem Aufruf von control.events auf, mit der neuen Liste, den geänderten und entfernten Objekten und einem Grund. Geben Sie dem Control eine Kopie Ihres Arrays, denn es übernimmt das Array und bearbeitet es direkt. Die Bibliothek spricht nie mit Ihrem Backend: Speichern Sie in onEventMove, um vor dem Übernehmen der Änderung zu bestätigen, oder in onEventsChange, um optimistisch zu speichern und bei einem Fehler zurückzusetzen.

SuperScheduler Pro kennt zwei Arten, Ereignisdaten zu besitzen. **Kontrolliert**: Der React-State ist die maßgebliche Quelle, Sie übergeben ihn als `events`, und `onEventsChange` teilt Ihnen mit, was der Nutzer oder die API geändert hat. **Unkontrolliert**: Sie übergeben Anfangsdaten mit `defaultEvents`, und das Control führt seine eigene Liste. Kontrolliert ist der richtige Standard für eine Anwendung, die Änderungen speichert, sie an anderer Stelle der Seite anzeigt oder Rückgängig machen unterstützt.

Diese Seite erklärt das kontrollierte Muster, was der Änderungs-Callback erhält, die Regeln zum Besitz des Arrays, auf denen alles beruht, die genaue Reihenfolge der Callbacks beim Ablegen und an welcher Stelle Ihr Backend ins Spiel kommt.

## Das kontrollierte Muster
```tsx
// src/ControlledPlanning.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SuperScheduler } from 'super-scheduler'

const ROOMS: SuperScheduler.ResourceData[] = [
  { id: 'r101', name: 'Room 101' },
  { id: 'r102', name: 'Room 102' },
]
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Month' },
  { groupBy: 'Day', format: 'd' },
]

const INITIAL: SuperScheduler.EventData[] = [
  {
    id: 'b-1042',
    resource: 'r101',
    start: '2026-10-02T14:00:00',
    end: '2026-10-05T11:00:00',
    text: 'Booking 1042',
  },
  {
    id: 'b-1043',
    resource: 'r102',
    start: '2026-10-03T14:00:00',
    end: '2026-10-08T11:00:00',
    text: 'Booking 1043',
  },
]

export function ControlledPlanning() {
  // React state is the single source of truth for the events.
  const [events, setEvents] = useState<SuperScheduler.EventData[]>(INITIAL)

  // The control adopts the array it receives and splices it in place: give it its own copy.
  const owned = useMemo(() => events.slice(), [events])

  // Once per task, after a drop, a resize or a control.events call. Handing the same objects back
  // is recognised as an echo: the control does not reload or repaint.
  const onEventsChange = useCallback((args: SchedulerEventsChangeArgs) => {
    setEvents([...args.events])
  }, [])

  // Changes made outside the scheduler go to state; the control picks up the new array.
  const addBlock = () =>
    setEvents((current) => [
      ...current,
      {
        id: `block-${crypto.randomUUID()}`,
        resource: 'r102',
        start: '2026-10-12T00:00:00',
        end: '2026-10-14T00:00:00',
        text: 'Maintenance',
        moveDisabled: true,
        resizeDisabled: true,
      },
    ])

  return (
    <>
      <p>
        {events.length} events{' '}
        <button type="button" onClick={addBlock}>
          Block Room 102
        </button>
      </p>
      <SuperSchedulerComponent
        startDate="2026-10-01"
        days={31}
        scale="Day"
        timeHeaders={TIME_HEADERS}
        resources={ROOMS}
        events={owned}
        onEventsChange={onEventsChange}
      />
    </>
  )
}
```
Sie sollten zwei Buchungen und einen Ereigniszähler sehen. Ziehen Sie eine Buchung in das andere Zimmer: Sie bleibt dort, wo Sie sie abgelegt haben, weil ihre neue Position jetzt im React-State steht. Drücken Sie den Button: Der Zähler steigt, und auf Room 102 erscheint ein Wartungsblock, gesperrt gegen Verschieben und Dauer ändern.

Drei Zeilen tragen das Muster:

1. `useState` hält die Ereignisse. Alles, was sie anzeigt oder bearbeitet, liest diesen State.
2. `useMemo(() => events.slice(), [events])` gibt dem Control **seine eigene Kopie** des Arrays (siehe [Besitz des Arrays](#ownership)).
3. `onEventsChange` schreibt die neue Liste des Controls mit `setEvents([...args.events])` in den State zurück.

Ändert sich der State aus einem anderen Grund (ein Formular, ein Server-Push, der Button oben), erreicht das neue Array das Control als geänderte Prop, und das Control lädt es neu.

## Was onEventsChange erhält
`onEventsChange` wird aufgerufen, nachdem sich der Ereignisspeicher des Controls geändert hat, und zwar **höchstens einmal pro Task**: Mehrere Änderungen im selben synchronen Block kommen gemeinsam in einem Aufruf an, im nächsten Microtask.

| Argument | Inhalt |
|---|---|
| `events` | Die vollständige Liste des Controls nach der Änderung, als Datenobjekte |
| `changed` | Durch diese Änderung hinzugefügte oder ersetzte Objekte, in ihrem neuen Zustand |
| `removed` | Durch diese Änderung entfernte oder ersetzte Objekte, in ihrem vorherigen Zustand |
| `reason` | Warum sich der Speicher geändert hat (siehe unten) |

| `reason` | Ausgelöst durch |
|---|---|
| `'move'` | Ein abgeschlossenes Drag-and-drop, auch per Tastatur, sowie Ablegen von außerhalb des Planers |
| `'resize'` | Eine abgeschlossene Dauer-Änderung |
| `'create'` | `control.events.add()` |
| `'update'` | `control.events.update()` mit einem neuen Objekt oder ein Hinzufügen und ein Entfernen im selben Task |
| `'remove'` | `control.events.remove()`, einschließlich des eingebauten Lösch-Buttons (`eventDeleteHandling: 'Update'`) |
| `'history'` | Rückgängig machen oder Wiederholen, vom Control über `super-scheduler/history` angewendet |
| `'load'` | Sie haben andere Ereignisobjekte als `events` übergeben, oder ein Bereichslader hat neu geladene Ereignisse eingefügt |
| `'api'` | Andere Änderungen, die die Bibliothek von sich aus am Speicher vornimmt; behandeln Sie sie wie `'update'` |

Bei einer Verschiebung enthält `changed` das neue Objekt und `removed` das Objekt, das es ersetzt hat: Sie haben den Zustand vorher und nachher, ohne eine eigene Kopie zu führen. Die Objekte in `events` behalten ihre Identität zwischen den Aufrufen, sofern sie sich nicht geändert haben; `React.memo` und Selektoren, die per Referenz vergleichen, funktionieren also weiter.

## Unkontrolliert: defaultEvents
Übergeben Sie `defaultEvents` statt `events`, wenn der Planer die Daten besitzen darf, etwa in einer überwiegend lesenden Ansicht oder einem Prototyp. Das Array wird einmal gelesen, bei der Initialisierung; spätere Änderungen der Prop werden mit einer Warnung in der Entwicklung ignoriert. Übergeben Sie beide, gewinnt `events`, ebenfalls mit einer Warnung.

In diesem Modus lesen Sie die aktuellen Daten aus `control.events.list`, abonnieren sie mit `useScheduler({ track: ['events'] })` aus `super-scheduler/hooks` oder hören weiterhin auf `onEventsChange`, das in beiden Modi funktioniert.

## Das Control übernimmt Ihr Array
Aus Geschwindigkeitsgründen kopiert das Control das Array, das Sie als `events` übergeben, nicht: `control.events.list` **ist** dieses Array, und Hinzufügen, Entfernen und Ablegen bearbeiten es direkt mit `splice`. Deshalb übergibt das Muster oben eine Kopie. Ohne die Kopie würde das Control das Array in Ihrem React-State verändern, an React vorbei.

Dieselbe Regel erklärt das übrige Verhalten der kontrollierten Schleife:

- **Echos kosten nichts.** Speichert `onEventsChange` `[...args.events]`, rendert React, und das Control erhält ein Array mit genau den Objekten, die es bereits hält. Es erkennt das Echo und tut nichts: kein Neuladen, kein Neuzeichnen.
- **Neue Objekte lösen ein Neuladen aus.** Enthält Ihr State Objekte, die das Control noch nicht gesehen hat (eine Bearbeitung im Formular, eine Serverantwort), lädt es seine Liste aus dem neuen Array neu und meldet dann `reason: 'load'`. Diese Liste erneut zu speichern ist ein Echo, und die Schleife endet dort.
- **Frieren Sie das Array nicht ein**, wenn Sie `control.events.add`, `update` oder `remove` aufrufen: Sie bearbeiten es direkt und werfen bei einem eingefrorenen Array einen `TypeError`. Ablegen und Dauer ändern kopieren ein eingefrorenes Array vorher.

> **Behavior:**
> Ein Ablegen bearbeitet nie Ihr Ereignisobjekt: Die Bibliothek ersetzt es durch ein neues Objekt, `{ ...old, start, end, resource }`, dessen `start` und `end` `SuperScheduler.Date`-Werte sind. Anders die Setter des Ereignis-Wrappers (`e.start(value)`, `e.end(value)`): Sie schreiben in das bestehende Datenobjekt. Im kontrollierten Modus verwenden Sie besser `control.events.update({ ...e.data, end })` mit einem neuen Objekt oder ändern Ihren State direkt.

## Reihenfolge der Callbacks beim Ablegen
Jedes Drag-and-drop durchläuft eine feste Abfolge. Dieser Logger macht sie sichtbar:

```ts
// src/tracing.ts
import type { SchedulerProps } from 'super-scheduler'

// Logs every callback of one drag-and-drop, in the order the library calls them.
export const tracing: SchedulerProps = {
  onEventMoving: (args) =>
    console.debug('1. moving (every shadow change)', args.start.value, args.allowed),
  onEventMove: (args) =>
    console.debug('2. move (before the commit, cancelable)', args.newStart.value),
  onEventMoved: (args) =>
    // The store already holds the new times here.
    console.debug(
      '3. moved (after the commit)',
      args.control.events.find(args.e.id())?.start().value,
    ),
  onEventsChange: (args) =>
    console.debug('4. eventsChange (next microtask)', args.reason, args.changed.length),
}
```
1. **`onEventMoving`** läuft bei jeder Änderung des Schattens, während der Nutzer zieht. Es kann die Position ablehnen oder anpassen (siehe [Ziehen, Dauer ändern und Geschäftsregeln](https://superscheduler.org/de/docs/drag-resize-rules/)).
2. Wurde beim Loslassen die letzte Position abgelehnt (durch Ihre Regel, eine Überlappung, eine gesperrte Zelle), **läuft nichts weiter**: kein `onEventMove`, keine Änderung.
3. **`onEventMove`** läuft einmal, bevor sich der Speicher ändert. Es kann mit `args.preventDefault()` abbrechen, `args.newStart`, `args.newEnd` oder `args.newResource` ändern oder die Entscheidung mit `args.async = true` und `args.loaded()` aufschieben.
4. Der Speicher wird aktualisiert (mit `eventMoveHandling: 'Update'`, dem Standard).
5. **`onEventMoved`** läuft nach dem Übernehmen: `args.control.events.find(id)` liefert bereits die neuen Zeiten.
6. **`onEventsChange`** läuft im nächsten Microtask mit `reason: 'move'`.

Das Ändern der Dauer folgt derselben Abfolge mit `onEventResizing`, `onEventResize`, `onEventResized` und `reason: 'resize'`. Da `onEventMoved` läuft, bevor React etwas gespeichert hat, lesen Sie die neuen Werte aus seinen Argumenten, nicht aus Ihrem State.

## Wo Ihr Backend ins Spiel kommt
Der Planer ruft nie einen Server auf. Sie entscheiden, wann gespeichert wird, und es gibt zwei solide Ansätze.

### Bestätigen, bevor die Änderung übernommen wird
Speichern Sie in `onEventMove` oder `onEventResize` mit `args.async = true` und rufen Sie `args.loaded()` auf, wenn der Server antwortet; hat er abgelehnt, rufen Sie vorher `args.preventDefault()` auf. Bis dahin bleibt das Ereignis, wo es war, der Bildschirm zeigt also nie eine Änderung, die der Server abgelehnt hat. Der Preis ist eine sichtbare Verzögerung bei jedem Ablegen. Das vollständige Muster steht unter [Beim Ablegen bestätigen](https://superscheduler.org/de/docs/drag-resize-rules/#async-confirmation).

### Optimistisch speichern und bei einem Fehler zurücksetzen
Übernehmen Sie die Änderung sofort, speichern Sie im Hintergrund und setzen Sie das vorherige Objekt wieder ein, wenn das Speichern fehlschlägt. `onEventsChange` liefert alles, was Sie brauchen: `changed` ist das, was gespeichert werden muss, `removed` das, was wiederherzustellen ist.

```tsx
// src/OptimisticPlanning.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SuperScheduler } from 'super-scheduler'

const iso = (value: SuperScheduler.DateInput) => (typeof value === 'string' ? value : value.value)

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly initial: SuperScheduler.EventData[]
}

export function OptimisticPlanning({ rooms, initial }: Props) {
  const [events, setEvents] = useState(initial)
  const owned = useMemo(() => events.slice(), [events])
  const { controlRef } = useSchedulerControl()

  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => {
      // 1. Show the change immediately.
      setEvents([...args.events])
      if (args.reason !== 'move' && args.reason !== 'resize') return

      for (const after of args.changed) {
        // The object this drop replaced: the state to restore if the server says no.
        const before = args.removed.find((item) => item.id === after.id)
        if (before === undefined || after.resource === undefined) continue

        // 2. Persist it.
        saveBooking({
          id: String(after.id),
          resource: String(after.resource),
          start: iso(after.start),
          end: iso(after.end),
        })
          // 3. Revert on failure. Matching by identity leaves a newer change of the same event alone.
          .catch(() => {
            setEvents((current) => current.map((item) => (item === after ? before : item)))
            controlRef.current?.message('The change could not be saved and was undone.')
          })
      }
    },
    [controlRef],
  )

  return (
    <SuperSchedulerComponent
      controlRef={controlRef}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      resources={rooms}
      events={owned}
      onEventsChange={onEventsChange}
    />
  )
}
```
Ein Ablegen sollte sofort wirken. Lehnt `saveBooking` ab, springt die Buchung an ihren vorherigen Platz zurück, und eine Meldung erklärt den Grund. Das Zurücksetzen gleicht per Objektidentität ab; hat der Nutzer dieselbe Buchung inzwischen erneut verschoben, bleibt die neuere Änderung unangetastet.

Für welchen Ansatz Sie sich auch entscheiden, diese Aufgaben bleiben in Ihrer Anwendung:

- **Auf dem Server validieren.** Regeln in `onEventMoving` dienen der Benutzerführung; der Server muss Überlappungen, Berechtigungen und Geschäftsregeln erneut prüfen, weil andere Nutzer und andere Clients dieselben Daten ändern.
- **Normalisieren, was Sie senden.** Verschobene Ereignisse tragen `SuperScheduler.Date`-Werte, unberührte Ihre Strings. Siehe [Werte nach dem Ziehen](https://superscheduler.org/de/docs/resources-events-intervals/#after-drag).
- **Die Version des Servers übernehmen.** Gibt der Server ein kanonisches Objekt zurück (eine neue ID für ein angelegtes Ereignis, einen neu berechneten Preis), ersetzen Sie das Objekt im State. Das Control lädt neu und meldet `reason: 'load'`.

## Rückgängig machen, Bereiche und bereichsweises Laden
- **Rückgängig machen und Wiederholen.** `createHistory({ apply })` aus `super-scheduler/history` kann Rückgängig machen und Wiederholen auf Ihren State statt auf das Control anwenden. Siehe [Rückgängig machen und Wiederholen](https://superscheduler.org/de/docs/undo-redo/).
- **Mehrere Bereiche.** `SchedulerPanes` teilt eine Ereignisliste zwischen Bereichen über kontrollierte `events` und `onEventsChange` (oder `defaultEvents`). Siehe [Bereiche und gespeicherte Ansichten](https://superscheduler.org/de/docs/panes-saved-views/).
- **Laden nach Datumsbereich.** Ein Bereichslader aus `super-scheduler/ranges` fügt ein, was er lädt, und meldet es über `onEventsChange` mit `reason: 'load'`; übernehmen Sie diese Liste. Siehe [Bereichsweises Laden](https://superscheduler.org/de/docs/range-loading/).

→ https://superscheduler.org/de/examples/field-service-dispatch/
→ https://superscheduler.org/de/examples/training-rooms/
## Nächste Schritte
- Ungültige Verschiebungen schon während des Ziehens ablehnen: [Ziehen, Dauer ändern und Geschäftsregeln](https://superscheduler.org/de/docs/drag-resize-rules/).
- Eigene Felder durchgängig typisieren: [Eigene Felder mit EventData&lt;T&gt;](https://superscheduler.org/de/docs/resources-events-intervals/#custom-fields).
- Das Control aus React-Code erreichen: [React-Integration](https://superscheduler.org/de/docs/react-integration/#control).
