Concepts clésS’applique àSuperScheduler Pro
Événements contrôlés et callbacks
Passez les événements depuis le state React et réécrivez-les dans onEventsChange, que le contrôle appelle une fois par tâche après un dépôt, un redimensionnement ou un appel à control.events, avec la nouvelle liste, les objets modifiés et supprimés, et un motif. Donnez au contrôle une copie de votre tableau, car il adopte le tableau et le modifie sur place. La bibliothèque ne communique jamais avec votre backend : enregistrez depuis onEventMove pour confirmer avant que la modification soit validée, ou depuis onEventsChange pour enregistrer de façon optimiste et revenir en arrière en cas d’échec.
SuperScheduler Pro propose deux façons de posséder les données d’événements. Contrôlé : le state React est la source de vérité, vous le passez dans events, et onEventsChange vous indique ce que l’utilisateur ou l’API a modifié. Non contrôlé : vous fournissez les données initiales avec defaultEvents et le contrôle garde sa propre liste. Le mode contrôlé est le bon choix par défaut pour une application qui enregistre les modifications, les affiche ailleurs dans la page ou prend en charge l’annulation.
Cette page explique le modèle contrôlé, ce que reçoit le callback de modification, les règles de propriété du tableau qui le font fonctionner, l’ordre exact des callbacks lors d’un dépôt et le moment où intervient votre backend.
Le modèle contrôlé
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}
/>
</>
)
}Vous devriez voir deux réservations et un compteur d’événements. Faites glisser une réservation vers l’autre chambre : elle reste là où vous l’avez déposée, car sa nouvelle position est désormais dans le state React. Appuyez sur le bouton : le compteur augmente et un blocage de maintenance apparaît sur Room 102, verrouillé contre le déplacement et le redimensionnement.
Trois lignes portent le modèle :
useStatecontient les événements. Tout ce qui les affiche ou les modifie lit ce state.useMemo(() => events.slice(), [events])donne au contrôle sa propre copie du tableau (voir propriété du tableau).onEventsChangeréécrit dans le state la nouvelle liste du contrôle avecsetEvents([...args.events]).
Quand le state change pour une autre raison (un formulaire, un push du serveur, le bouton ci-dessus), le nouveau tableau atteint le contrôle comme une prop modifiée, et le contrôle le recharge.
Ce que reçoit onEventsChange
onEventsChange est appelé après une modification du stockage d’événements du contrôle, au plus une fois par tâche : plusieurs modifications effectuées dans le même bloc synchrone arrivent ensemble en un seul appel, à la microtâche suivante.
| Argument | Contenu |
|---|---|
events | La liste complète du contrôle après la modification, sous forme d’objets de données |
changed | Les objets ajoutés ou remplacés par cette modification, dans leur nouvel état |
removed | Les objets supprimés ou remplacés par cette modification, dans leur état précédent |
reason | Pourquoi le stockage a changé (ci-dessous) |
reason | Déclenché par |
|---|---|
'move' | Un glisser-déposer validé, y compris au clavier, et les dépôts venus de l’extérieur du planificateur |
'resize' | Un redimensionnement validé |
'create' | control.events.add() |
'update' | control.events.update() avec un nouvel objet, ou un ajout et une suppression dans la même tâche |
'remove' | control.events.remove(), y compris le bouton de suppression intégré (eventDeleteHandling: 'Update') |
'history' | Une annulation ou un rétablissement appliqué par le contrôle via super-scheduler/history |
'load' | Vous avez passé d’autres objets événements dans events, ou un chargeur de plages a fusionné des événements nouvellement chargés |
'api' | D’autres modifications que la bibliothèque apporte d’elle-même au stockage ; traitez-les comme 'update' |
Pour un déplacement, changed contient le nouvel objet et removed l’objet qu’il a remplacé : vous disposez de l’état avant et après sans garder votre propre copie. Les objets de events conservent leur identité d’un appel à l’autre tant qu’ils n’ont pas changé : React.memo et les sélecteurs qui comparent par référence continuent donc de fonctionner.
Non contrôlé : defaultEvents
Passez defaultEvents au lieu de events quand le planificateur peut posséder les données, par exemple dans une vue surtout consultée ou dans un prototype. Le tableau est lu une seule fois, à l’initialisation ; les modifications ultérieures de la prop sont ignorées, avec un avertissement en développement. Si vous passez les deux, events l’emporte, également avec un avertissement.
Dans ce mode, lisez les données actuelles dans control.events.list, abonnez-vous avec useScheduler({ track: ['events'] }) de super-scheduler/hooks, ou écoutez quand même onEventsChange, qui fonctionne dans les deux modes.
Le contrôle adopte votre tableau
Par souci de rapidité, le contrôle ne copie pas le tableau que vous passez dans events : control.events.list est ce tableau, et les ajouts, suppressions et dépôts le modifient sur place avec splice. C’est pourquoi le modèle ci-dessus passe une copie. Sans elle, le contrôle muterait le tableau contenu dans votre state React, à l’insu de React.
La même règle explique les autres comportements de la boucle contrôlée :
- Les échos ne coûtent rien. Quand
onEventsChangestocke[...args.events], React effectue un rendu et le contrôle reçoit un tableau contenant exactement les objets qu’il détient déjà. Il reconnaît l’écho et ne fait rien : ni rechargement, ni repaint. - De nouveaux objets provoquent un rechargement. Quand votre state contient des objets que le contrôle n’a jamais vus (une modification par formulaire, une réponse du serveur), il recharge sa liste depuis le nouveau tableau puis signale
reason: 'load'. Stocker à nouveau cette liste est un écho : la boucle s’arrête là. - Ne gelez pas le tableau si vous appelez
control.events.add,updateouremove: ils le modifient sur place et lèvent uneTypeErrorsur un tableau gelé. Les dépôts et les redimensionnements copient d’abord un tableau gelé.
Ordre des callbacks lors d’un dépôt
Chaque glisser-déposer suit une séquence fixe. Ce logger la met en évidence :
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),
}onEventMovings’exécute à chaque changement de l’ombre pendant que l’utilisateur fait glisser. Il peut refuser la position ou l’ajuster (voir Glisser, redimensionner et règles métier).- Au relâchement, si la dernière position a été refusée (par votre règle, un chevauchement, une cellule désactivée), rien d’autre ne s’exécute : ni
onEventMove, ni modification. onEventMoves’exécute une fois, avant la modification du stockage. Il peut annuler avecargs.preventDefault(), changerargs.newStart,args.newEndouargs.newResource, ou différer la décision avecargs.async = trueetargs.loaded().- Le stockage est mis à jour (avec
eventMoveHandling: 'Update', la valeur par défaut). onEventMoveds’exécute après la validation :args.control.events.find(id)renvoie déjà les nouveaux horaires.onEventsChanges’exécute à la microtâche suivante avecreason: 'move'.
Le redimensionnement suit la même séquence avec onEventResizing, onEventResize, onEventResized et reason: 'resize'. Comme onEventMoved s’exécute avant que React ait stocké quoi que ce soit, lisez les nouvelles valeurs dans ses arguments, pas dans votre state.
Où intervient votre backend
Le planificateur n’appelle jamais de serveur. C’est vous qui décidez quand enregistrer, et deux conceptions solides s’offrent à vous.
Confirmer avant de valider la modification
Enregistrez dans onEventMove ou onEventResize avec args.async = true, et appelez args.loaded() quand le serveur répond ; appelez d’abord args.preventDefault() s’il a refusé. D’ici là, l’événement reste à sa place : l’écran n’affiche donc jamais une modification rejetée par le serveur. Le prix à payer est une latence visible à chaque dépôt. Le modèle complet se trouve dans Confirmer au dépôt.
Enregistrer de façon optimiste et revenir en arrière en cas d’échec
Acceptez la modification immédiatement, enregistrez en arrière-plan et remettez l’objet précédent si l’enregistrement échoue. onEventsChange fournit tout le nécessaire : changed est ce qu’il faut enregistrer, removed ce qu’il faut restaurer.
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}
/>
)
}Un dépôt devrait prendre effet immédiatement. Si saveBooking est rejeté, la réservation revient à sa place précédente et un message en explique la raison. Le retour arrière s’appuie sur l’identité des objets : si l’utilisateur a déplacé la même réservation entre-temps, la modification plus récente n’est pas touchée.
Quelle que soit la conception choisie, gardez ces responsabilités dans votre application :
- Validez côté serveur. Les règles de
onEventMovingrelèvent de l’expérience utilisateur ; le serveur doit vérifier de nouveau les chevauchements, les permissions et les règles métier, car d’autres utilisateurs et d’autres clients modifient les mêmes données. - Normalisez ce que vous envoyez. Les événements déplacés portent des valeurs
SuperScheduler.Date; ceux qui n’ont pas été touchés portent vos chaînes. Voir les valeurs après un glisser. - Adoptez la version du serveur. Si le serveur renvoie un objet canonique (un nouvel id pour un événement créé, un prix recalculé), remplacez l’objet dans le state. Le contrôle recharge et signale
reason: 'load'.
Annulation, volets et chargement par plages
- Annuler et rétablir.
createHistory({ apply })desuper-scheduler/historypeut appliquer l’annulation et le rétablissement à votre state plutôt qu’au contrôle. Voir Annuler et rétablir. - Plusieurs volets.
SchedulerPanespartage une même liste d’événements entre les volets viaeventscontrôlé etonEventsChange(oudefaultEvents). Voir Volets et vues enregistrées. - Chargement par plage de dates. Un chargeur de plages de
super-scheduler/rangesfusionne ce qu’il charge et le signale viaonEventsChangeavecreason: 'load'; adoptez cette liste. Voir Chargement par plages.
Répartition des interventions techniquesUne intervention urgente arrive. Trouvez l’équipe qui peut la prendre avant l’échéance. Planification de salles de formationLes inscriptions dépassent la salle. Sélectionnez les deux sessions, voyez ce qui est libre pour les deux, déplacez-les ensemble et gardez la vue.
Étapes suivantes
- Refusez les déplacements invalides pendant que l’utilisateur fait glisser : Glisser, redimensionner et règles métier.
- Typez vos champs personnalisés de bout en bout : Champs personnalisés avec EventData<T>.
- Accédez au contrôle depuis le code React : Intégration React.
Exemples liés
- FieldworkRépartition des interventions techniquesUne intervention urgente arrive. Trouvez l’équipe qui peut la prendre avant l’échéance.
- MorrowPlanification de salles de formationLes inscriptions dépassent la salle. Sélectionnez les deux sessions, voyez ce qui est libre pour les deux, déplacez-les ensemble et gardez la vue.