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.
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
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:
useStateguarda los eventos. Todo lo que los muestra o los edita lee este estado.useMemo(() => events.slice(), [events])da al control su propia copia del array (consulta propiedad del array).onEventsChangeescribe la lista nueva del control de vuelta en el estado consetEvents([...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.
| Argumento | Contenido |
|---|---|
events | La lista completa del control después del cambio, como objetos de datos |
changed | Los objetos añadidos o sustituidos en este cambio, en su nuevo estado |
removed | Los objetos eliminados o sustituidos en este cambio, en su estado anterior |
reason | Por qué cambió el almacén (abajo) |
reason | Lo 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
onEventsChangeguarda[...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,updateoremove: lo editan en el sitio y lanzan unTypeErrorsobre 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:
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),
}onEventMovingse 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).- 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
onEventMoveni cambio alguno. onEventMovese ejecuta una vez, antes de que cambie el almacén. Puede cancelar conargs.preventDefault(), cambiarargs.newStart,args.newEndoargs.newResource, o aplazar la decisión conargs.async = trueyargs.loaded().- Se actualiza el almacén (con
eventMoveHandling: 'Update', el valor por defecto). onEventMovedse ejecuta tras la confirmación:args.control.events.find(id)ya devuelve las horas nuevas.onEventsChangese ejecuta en la siguiente microtarea conreason: '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.
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
onEventMovingson 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, paneles y carga por rangos
- Deshacer y rehacer.
createHistory({ apply })desuper-scheduler/historypuede aplicar deshacer y rehacer a tu estado en lugar de al control. Consulta Deshacer y rehacer. - Varios paneles.
SchedulerPanescomparte una lista de eventos entre paneles medianteeventsyonEventsChangecontrolados (odefaultEvents). Consulta Paneles y vistas guardadas. - Carga por rango de fechas. Un cargador de rangos de
super-scheduler/rangesfusiona lo que carga y lo comunica medianteonEventsChangeconreason: '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
- Rechaza movimientos no válidos mientras el usuario arrastra: Arrastrar, redimensionar y reglas de negocio.
- Tipa tus campos propios de principio a fin: Campos propios con EventData<T>.
- Accede al control desde código React: Integración con React.