Saltar al contenido
SuperScheduler

Conceptos básicosSe aplica aSuperScheduler Pro

Eventos controlados y callbacks

Pasa los eventos desde el estado de React y escríbelos de vuelta en onEventsChange, que el control llama una vez por tarea después de soltar, redimensionar o una llamada a control.events, con la lista nueva, los objetos cambiados y eliminados, y un motivo. Da al control una copia de tu array, porque el control lo adopta y lo edita en el sitio. La librería nunca habla con tu backend: guarda desde onEventMove para confirmar antes de que el cambio se aplique, o desde onEventsChange para guardar de forma optimista y revertir si falla.

Verificado con v0.1.0 · revisado el 7 de octubre de 2026.md

SuperScheduler Pro admite dos formas de ser dueño de los datos de eventos. Controlado: el estado de React es la fuente de verdad, lo pasas como events y onEventsChange te dice qué han cambiado el usuario o la API. No controlado: entregas los datos iniciales con defaultEvents y el control mantiene su propia lista. El modo controlado es la opción por defecto adecuada para una aplicación que guarda los cambios, los muestra en otra parte de la página o permite deshacer.

Esta página explica el patrón controlado, qué recibe el callback de cambios, las reglas de propiedad del array que lo hacen funcionar, el orden exacto de los callbacks al soltar un evento y dónde entra tu backend.

El patrón controlado

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

Deberías ver dos reservas y un contador de eventos. Arrastra una reserva a la otra habitación: se queda donde la sueltas, porque su nueva posición ya está en el estado de React. Pulsa el botón: el contador sube y aparece un bloque de mantenimiento en Room 102, bloqueado para que no se pueda mover ni redimensionar.

Tres líneas sostienen el patrón:

  1. useState guarda los eventos. Todo lo que los muestra o los edita lee este estado.
  2. useMemo(() => events.slice(), [events]) da al control su propia copia del array (consulta propiedad del array).
  3. onEventsChange escribe la lista nueva del control de vuelta en el estado con setEvents([...args.events]).

Cuando el estado cambia por otro motivo (un formulario, un push del servidor, el botón de arriba), el array nuevo llega al control como una prop cambiada y el control lo recarga.

Qué recibe onEventsChange

onEventsChange se llama después de que cambie el almacén de eventos del control, como mucho una vez por tarea: varios cambios hechos en el mismo bloque síncrono llegan juntos en una sola llamada, en la siguiente microtarea.

ArgumentoContenido
eventsLa lista completa del control después del cambio, como objetos de datos
changedLos objetos añadidos o sustituidos en este cambio, en su nuevo estado
removedLos objetos eliminados o sustituidos en este cambio, en su estado anterior
reasonPor qué cambió el almacén (abajo)
reasonLo provoca
'move'Un arrastrar y soltar confirmado, también con el teclado, y lo que se suelta desde fuera del scheduler
'resize'Un redimensionado confirmado
'create'control.events.add()
'update'control.events.update() con un objeto nuevo, o un alta y una baja en la misma tarea
'remove'control.events.remove(), incluido el botón de borrado integrado (eventDeleteHandling: 'Update')
'history'Deshacer o rehacer aplicado por el control mediante super-scheduler/history
'load'Pasaste objetos de evento distintos como events, o un cargador de rangos fusionó eventos recién cargados
'api'Otros cambios que la librería hace en el almacén por su cuenta; trátalos como 'update'

En un movimiento, changed contiene el objeto nuevo y removed el objeto al que sustituyó: tienes el estado anterior y el posterior sin guardar tu propia copia. Los objetos de events conservan su identidad entre llamadas salvo que hayan cambiado, así que React.memo y los selectores que comparan por referencia siguen funcionando.

No controlado: defaultEvents

Pasa defaultEvents en lugar de events cuando el scheduler pueda ser el dueño de los datos, por ejemplo en una vista casi de solo lectura o en un prototipo. El array se lee una sola vez, durante la inicialización; los cambios posteriores de la prop se ignoran, con un aviso en desarrollo. Si pasas ambas, gana events, también con un aviso.

En este modo, lee los datos actuales de control.events.list, suscríbete con useScheduler({ track: ['events'] }) de super-scheduler/hooks o sigue escuchando onEventsChange, que funciona en ambos modos.

El control adopta tu array

Por rendimiento, el control no copia el array que pasas como events: control.events.list es ese array, y las altas, las bajas y las operaciones de soltar lo editan en el sitio con splice. Por eso el patrón de arriba pasa una copia. Sin ella, el control mutaría el array que hay dentro de tu estado de React, a espaldas de React.

La misma regla explica los demás comportamientos del bucle controlado:

  • Los ecos no cuestan nada. Cuando onEventsChange guarda [...args.events], React renderiza y el control recibe un array que contiene exactamente los objetos que ya tiene. Reconoce el eco y no hace nada: ni recarga ni repinta.
  • Los objetos nuevos provocan una recarga. Cuando tu estado contiene objetos que el control no ha visto (una edición en un formulario, una respuesta del servidor), recarga su lista desde el array nuevo y después informa con reason: 'load'. Volver a guardar esa lista es un eco, así que el bucle termina ahí.
  • No congeles el array si llamas a control.events.add, update o remove: lo editan en el sitio y lanzan un TypeError sobre un array congelado. Al soltar y al redimensionar, primero se copia el array congelado.

Orden de los callbacks al soltar

Cada arrastrar y soltar sigue una secuencia fija. Este logger la muestra:

src/tracing.tsts
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 se ejecuta en cada cambio de la sombra mientras el usuario arrastra. Puede rechazar la posición o ajustarla (consulta Arrastrar, redimensionar y reglas de negocio).
  2. Al soltar, si la última posición fue rechazada (por tu regla, por un solape, por una celda deshabilitada), no se ejecuta nada más: ni onEventMove ni cambio alguno.
  3. onEventMove se ejecuta una vez, antes de que cambie el almacén. Puede cancelar con args.preventDefault(), cambiar args.newStart, args.newEnd o args.newResource, o aplazar la decisión con args.async = true y args.loaded().
  4. Se actualiza el almacén (con eventMoveHandling: 'Update', el valor por defecto).
  5. onEventMoved se ejecuta tras la confirmación: args.control.events.find(id) ya devuelve las horas nuevas.
  6. onEventsChange se ejecuta en la siguiente microtarea con reason: 'move'.

El redimensionado sigue la misma secuencia con onEventResizing, onEventResize, onEventResized y reason: 'resize'. Como onEventMoved se ejecuta antes de que React haya guardado nada, lee los valores nuevos de sus argumentos, no de tu estado.

Dónde entra tu backend

El scheduler nunca llama a un servidor. Tú decides cuándo guardar, y hay dos diseños sólidos.

Confirmar antes de aplicar el cambio

Guarda dentro de onEventMove o onEventResize con args.async = true y llama a args.loaded() cuando responda el servidor; si lo rechazó, llama antes a args.preventDefault(). Hasta entonces, el evento se queda donde estaba, así que la pantalla nunca muestra un cambio que el servidor haya rechazado. El precio es una latencia visible en cada operación de soltar. El patrón completo está en Confirmar al soltar.

Guardar de forma optimista y revertir si falla

Acepta el cambio en el acto, guarda en segundo plano y vuelve a poner el objeto anterior si el guardado falla. onEventsChange tiene todo lo necesario: changed es lo que hay que guardar y removed lo que hay que restaurar.

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

Deberías ver que soltar surte efecto de inmediato. Si saveBooking rechaza la promesa, la reserva vuelve a su sitio anterior y un mensaje explica por qué. La reversión compara por identidad de objeto, así que si el usuario ha vuelto a mover la misma reserva entretanto, el cambio más reciente no se toca.

Elijas el diseño que elijas, mantén estas responsabilidades en tu aplicación:

  • Valida en el servidor. Las reglas de onEventMoving son experiencia de usuario; el servidor debe volver a comprobar solapes, permisos y reglas de negocio, porque otros usuarios y otros clientes cambian los mismos datos.
  • Normaliza lo que envías. Los eventos movidos llevan valores SuperScheduler.Date; los que nadie ha tocado llevan tus cadenas. Consulta valores después de arrastrar.
  • Adopta la versión del servidor. Si el servidor devuelve un objeto canónico (un id nuevo para un evento creado, un precio recalculado), sustituye el objeto en el estado. El control lo recarga e informa con reason: 'load'.
  • Deshacer y rehacer. createHistory({ apply }) de super-scheduler/history puede aplicar deshacer y rehacer a tu estado en lugar de al control. Consulta Deshacer y rehacer.
  • Varios paneles. SchedulerPanes comparte una lista de eventos entre paneles mediante events y onEventsChange controlados (o defaultEvents). Consulta Paneles y vistas guardadas.
  • Carga por rango de fechas. Un cargador de rangos de super-scheduler/ranges fusiona lo que carga y lo comunica mediante onEventsChange con reason: 'load'; adopta esa lista. Consulta Carga por rangos.

Despacho de servicio técnicoEntra un trabajo urgente en la cola. Encuentra la cuadrilla que puede atenderlo a tiempo. Planificación de aulas de formaciónLa matrícula superó el aula. Selecciona ambas sesiones, mira qué está libre para las dos, muévelas juntas y conserva la vista.

Siguientes pasos