Aller au contenu
SuperScheduler

ProductionS’applique àLite et Pro

Rendu serveur et pages prérendues

Côté serveur, les composants Pro et Lite rendent tous deux un `<div>` vide ; le planificateur est construit dans le navigateur une fois le composant monté. Rendez côté serveur une coquille de même hauteur, avec du vrai contenu comme les prochaines réservations, puis montez le planificateur côté client, idéalement depuis un module importé à la demande. Les paquets peuvent être importés côté serveur, s’hydratent sans incohérence et n’exigent de votre Content Security Policy ni scripts inline ni styles inline.

Vérifié avec la v0.1.0 · relu le 7 octobre 2026.md

SuperScheduler dessine sa grille avec un moteur DOM impératif, et ce moteur a besoin d’un navigateur : il mesure la zone visible, écoute les événements de défilement et de pointeur, et positionne les nœuds au fil du défilement. Le rendu serveur et le prérendu statique fonctionnent pourtant bien avec lui, à condition de décider ce que le serveur envoie pendant que le navigateur construit la vraie grille. Cela vaut pour les deux éditions.

Ce que rend le serveur

SuperSchedulerComponent, issu de super-scheduler, super-scheduler/react-render ou super-scheduler-lite, rend un unique <div> vide côté serveur. Le contrôle est créé dans componentDidMount, qui ne s’exécute jamais côté serveur ; par conséquent :

  • le HTML prérendu ne contient ni lignes, ni événements, ni en-têtes ;
  • ref.current.control et controlRef restent vides jusqu’à ce que le navigateur monte le composant ;
  • importer les paquets côté serveur ne pose aucun problème : aucun module ne touche window ou document au moment de l’import, modules Pro par sous-chemin compris ;
  • l’hydratation concorde : le premier rendu du navigateur est le même <div> vide, et le contrôle le remplit après l’hydratation.

Livrer une coquille utile

Une boîte vide jusqu’à l’exécution de JavaScript, c’est un premier affichage médiocre et une page vide pour les robots d’indexation et pour les lecteurs sans JavaScript. Rendez plutôt une page de substitution côté serveur :

  • Réservez l’espace. Donnez au conteneur, en CSS, la hauteur qu’aura le planificateur, pour que rien ne bouge en dessous quand la grille apparaît (pas de décalage de mise en page).
  • Montrez du vrai contenu. Un titre, les libellés de la barre d’outils et une courte liste des réservations du jour ou à venir indiquent aux visiteurs et aux moteurs de recherche à quoi sert la page. La coquille peut utiliser les mêmes données que le planificateur.
  • Signalez le chargement. aria-busy="true" sur la coquille indique aux technologies d’assistance que la région est encore en construction.
  • Gardez les parties statiques à l’extérieur. Titres, légendes et filtres qui ne dépendent pas du contrôle peuvent être rendus côté serveur une fois pour toutes et rester en place quand la grille se monte.

Les pages d’exemples de ce site fonctionnent ainsi : chacune est prérendue avec un aperçu statique de la première vue, et le planificateur interactif le remplace quand le visiteur lance la démo.

Planning des chambres d’hôtelLa douche de la 104 fuit. Relogez le prochain client, bloquez la chambre pour le plombier et repérez les nuits déjà complètes.

Monter côté client

Rendez la coquille côté serveur et pendant l’hydratation, puis passez au planificateur. useSyncExternalStore avec un instantané serveur à false donne un indicateur qui vaut false aux deux endroits et true juste après l’hydratation, sans incohérence. Charger le planificateur avec React.lazy garde son code hors du premier bundle ; la coquille sert aussi de fallback Suspense pendant le téléchargement du chunk.

src/PlanningPage.tsxtsx
import { Suspense, lazy, useSyncExternalStore } from 'react'
import { SuperScheduler } from 'super-scheduler'

// Fetched in the browser only, after hydration: the scheduler stays out of the page's first bundle.
const Planning = lazy(() => import('./Planning'))

const subscribe = () => () => {}

/** False on the server and during hydration, true afterwards: no hydration mismatch. */
function useIsClient(): boolean {
  return useSyncExternalStore(
    subscribe,
    () => true,
    () => false,
  )
}

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

export function PlanningPage({ rooms, events }: Props) {
  const isClient = useIsClient()
  const shell = <PlanningShell rooms={rooms} events={events} />
  return (
    // .planning reserves the scheduler's height in CSS, so the page does not move when it mounts.
    <section className="planning" aria-label="Room planning">
      {isClient ? (
        <Suspense fallback={shell}>
          <Planning rooms={rooms} events={events} />
        </Suspense>
      ) : (
        shell
      )}
    </section>
  )
}

/** Server-rendered stand-in with real content, at the same size as the grid. */
function PlanningShell({ rooms, events }: Props) {
  const roomName = new Map(rooms.map((room) => [room.id, room.name]))
  return (
    <div className="planning__shell" aria-busy="true">
      <h2>Upcoming stays</h2>
      <ul>
        {events.slice(0, 12).map((event) => (
          <li key={String(event.id)}>
            {new SuperScheduler.Date(event.start).toString('d MMM', 'en-us')}
            {' · '}
            {event.resource === undefined ? '' : roomName.get(event.resource)}
            {' · '}
            {event.text}
          </li>
        ))}
      </ul>
    </div>
  )
}
src/Planning.tsxtsx
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Month' },
  { groupBy: 'Day', format: 'd' },
]

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

/** Loaded with React.lazy, so it needs a default export. */
export default function Planning({ rooms, events }: Props) {
  // The control splices the array it receives: give it its own copy.
  const owned = useMemo(() => events.slice(), [events])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={31}
      scale="Day"
      cellWidth={44}
      // Fills .planning, whose height is fixed in CSS.
      height="100%"
      timeHeaders={TIME_HEADERS}
      resources={rooms}
      events={owned}
    />
  )
}

height="100%" remplit l’élément hôte du composant, qui est un <div> sans style : dimensionnez-le donc en CSS :

csscss
.planning {
  height: 560px;
}
/* The scheduler's host element and the shell fill the reserved box. */
.planning > div {
  height: 100%;
}
.planning__shell {
  overflow: auto;
}

Vous devriez voir la liste des séjours dans le code source de la page et au premier affichage, puis la grille dans la même boîte un instant après que la page devient interactive, sans décalage du contenu situé en dessous.

Importez super-scheduler/styles.css (ou super-scheduler-lite/styles.css) une seule fois depuis votre layout racine ou votre feuille de style globale, pour qu’il fasse partie du CSS que le serveur référence déjà. L’importer depuis le module chargé à la demande fonctionne aussi quand votre bundler découpe le CSS par chunk.

Notes par framework

Next.js

Aucun des deux paquets ne marque ses modules avec la directive 'use client'. Dans l’App Router, rendez le planificateur depuis votre propre Client Component : un fichier qui commence par 'use client' et importe SuperSchedulerComponent. Les Client Components sont tout de même rendus côté serveur : le planificateur arrive donc sous forme de <div> vide et le modèle de coquille ci-dessus s’applique tel quel. Pour ne pas rendre du tout ce composant côté serveur, chargez-le avec next/dynamic et { ssr: false, loading: () => <Shell /> } depuis un Client Component ; l’App Router n’accepte pas ssr: false dans les Server Components. Dans le Pages Router, next/dynamic avec ssr: false fonctionne directement dans une page. Importez la feuille de style dans le layout racine (App Router) ou dans pages/_app (Pages Router).

React Router et Remix

Les modules de route sont rendus côté serveur en mode SSR, et au moment du build quand vous prérendez : utilisez donc l’indicateur client et l’import à la demande présentés plus haut dans le composant de route. Les données de la coquille peuvent venir du loader de la route, ce qui garde les rendus serveur et client identiques. Importez la feuille de style depuis la route racine ou depuis votre CSS global.

Autres frameworks

La règle est partout la même : rendez un espace réservé dimensionné partout où le framework fait un rendu côté serveur, et ne montez le composant que dans le navigateur, par exemple comme îlot exécuté uniquement côté client.

Hydratation

La bibliothèque elle-même ne produit aucune incohérence d’hydratation. Les incohérences viennent en général de la coquille :

  • Ne calculez pas de dates à partir de l’horloge pendant le rendu. Le serveur et le navigateur peuvent ne pas être d’accord sur « aujourd’hui », ni sur le fuseau horaire. Passez les dates depuis votre loader, ou calculez-les après le montage.
  • Formatez les dates de la coquille avec une locale explicite, comme le fait toString('d MMM', 'en-us') ci-dessus, et jamais avec les réglages par défaut de l’appareil.
  • Sous StrictMode, le développement monte les composants deux fois ; le composant crée un contrôle neuf à chaque montage et libère le précédent.

Content Security Policy

SuperScheduler fonctionne sous une politique stricte :

  • Scripts. Aucun script inline, ni eval, ni new Function. Le code se charge sous forme de modules depuis votre bundle, y compris les chunks que Pro charge à la demande avec un import() dynamique (prise en charge du clavier, menus, bulles, packs de langue). script-src 'self', ou l’origine qui sert votre bundle, suffit.
  • Styles. La feuille de style est un fichier CSS ordinaire : style-src 'self' la couvre. Le moteur positionne les nœuds en écrivant dans element.style via le CSSOM, ce que style-src ne restreint pas, et il n’injecte aucun élément <style>. Vous n’avez pas besoin de 'unsafe-inline' pour la bibliothèque.
  • Images. La feuille de style Pro dessine quelques petites icônes et formes de squelette sous forme d’images SVG data:, comme le bouton de suppression d’un événement. Autorisez-les avec img-src 'self' data:, sinon ces décorations n’apparaîtront pas.
txttxt
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.com

Votre propre balisage rendu côté serveur suit les mêmes règles : la prop style de React devient un attribut style dans le HTML du serveur, qu’un style-src strict bloque avant l’hydratation. C’est pourquoi l’exemple dimensionne le conteneur avec une classe.

Liste de vérification

  • Coquille rendue côté serveur, avec la hauteur finale du planificateur réservée en CSS.
  • Planificateur monté uniquement dans le navigateur, depuis un module importé à la demande.
  • Feuille de style importée une seule fois depuis le layout racine ou le CSS global.
  • Aucune sortie dépendant de l’horloge ou de la locale de l’appareil dans les rendus serveur.
  • CSP avec script-src 'self', style-src 'self' et img-src 'self' data: ; des classes plutôt que des styles inline dans vos chaînes HTML.

Guides associés : Intégration React, Virtualisation et performances, Thèmes et Dépannage.