Zum Inhalt springen
SuperScheduler

ProduktionGilt fürLite und Pro

Server-Rendering und vorgerenderte Seiten

Auf dem Server rendern sowohl die Pro- als auch die Lite-Komponente ein leeres `<div>`; der Planer wird im Browser aufgebaut, nachdem die Komponente gemountet ist. Rendern Sie auf dem Server eine Hülle mit derselben Höhe und echtem Inhalt, etwa den nächsten Buchungen, und mounten Sie den Planer dann im Client, idealerweise aus einem per Lazy Import geladenen Modul. Die Pakete lassen sich auf dem Server importieren, hydrieren ohne Abweichungen und verlangen von Ihrer Content Security Policy weder Inline-Skripte noch Inline-Styles.

Geprüft mit v0.1.0 · überarbeitet am 7. Oktober 2026.md

SuperScheduler zeichnet sein Raster mit einer imperativen DOM-Engine, und diese Engine braucht einen Browser: Sie misst den sichtbaren Bereich, lauscht auf Scroll- und Zeigerereignisse und positioniert Knoten, während Sie scrollen. Server-Rendering und statisches Vorrendern funktionieren trotzdem gut damit, sofern Sie entscheiden, was der Server sendet, während der Browser das echte Raster aufbaut. Das gilt für beide Editionen.

Was der Server rendert

SuperSchedulerComponent, aus super-scheduler, super-scheduler/react-render oder super-scheduler-lite, rendert auf dem Server ein einzelnes leeres <div>. Das Control wird in componentDidMount erzeugt, das auf dem Server nie läuft. Daher gilt:

  • Das vorgerenderte HTML enthält keine Zeilen, Ereignisse oder Köpfe;
  • ref.current.control und controlRef bleiben leer, bis der Browser die Komponente mountet;
  • die Pakete auf dem Server zu importieren ist sicher: Kein Modul greift beim Import auf window oder document zu, auch nicht die Subpath-Module von Pro;
  • die Hydration stimmt überein: Das erste Rendern im Browser ist dasselbe leere <div>, und das Control füllt es nach der Hydration.

Eine sinnvolle Hülle ausliefern

Ein leerer Kasten, bis JavaScript läuft, ist ein schwacher erster Paint und für Crawler sowie für Leser ohne JavaScript eine leere Seite. Rendern Sie stattdessen auf dem Server einen Platzhalter:

  • Platz reservieren. Geben Sie dem Container per CSS die Höhe, die der Planer haben wird, damit sich nichts darunter verschiebt, wenn das Raster erscheint (kein Layout Shift).
  • Echten Inhalt zeigen. Eine Überschrift, die Beschriftungen der Werkzeugleiste und eine kurze Liste der heutigen oder kommenden Buchungen sagen Besuchern und Suchmaschinen, wozu die Seite dient. Die Hülle kann dieselben Daten wie der Planer verwenden.
  • Als ladend kennzeichnen. aria-busy="true" an der Hülle teilt assistiven Technologien mit, dass der Bereich noch aufgebaut wird.
  • Statische Teile draußen lassen. Titel, Legenden und Filter, die nicht vom Control abhängen, können dauerhaft auf dem Server gerendert werden und bleiben stehen, wenn das Raster gemountet wird.

Die Beispielseiten dieser Website funktionieren so: Jede wird mit einer statischen Vorschau der ersten Ansicht vorgerendert, und der Live-Planer ersetzt sie, wenn der Besucher die Demo startet.

Zimmerplanung im HotelIn Zimmer 104 tropft die Dusche. Buchen Sie den nächsten Gast um, sperren Sie das Zimmer für den Installateur und finden Sie die ausgebuchten Nächte.

Im Client mounten

Rendern Sie die Hülle auf dem Server und während der Hydration, und wechseln Sie dann zum Planer. useSyncExternalStore mit einem Server-Snapshot von false liefert ein Flag, das an beiden Stellen false ist und direkt nach der Hydration true, ohne Abweichung. Wenn Sie den Planer mit React.lazy laden, bleibt sein Code aus dem ersten Bundle heraus; die Hülle dient zugleich als Suspense-Fallback, während der Chunk heruntergeladen wird.

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%" füllt das Host-Element der Komponente, ein ungestyltes <div>; geben Sie ihm die Größe daher per 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;
}

Sie sollten die Liste der Aufenthalte im Seitenquelltext und beim ersten Paint sehen und dann, einen Moment nachdem die Seite interaktiv geworden ist, das Raster im selben Kasten, ohne dass sich der Inhalt darunter verschiebt.

Importieren Sie super-scheduler/styles.css (oder super-scheduler-lite/styles.css) einmal aus Ihrem Root-Layout oder Ihrem globalen Stylesheet, damit es Teil des CSS ist, das der Server bereits verlinkt. Der Import aus dem per Lazy Import geladenen Modul funktioniert ebenfalls, wenn Ihr Bundler CSS pro Chunk aufteilt.

Hinweise zu Frameworks

Next.js

Keines der beiden Pakete kennzeichnet seine Module mit der Direktive 'use client'. Rendern Sie den Planer im App Router aus Ihrer eigenen Client Component: einer Datei, die mit 'use client' beginnt und SuperSchedulerComponent importiert. Client Components werden trotzdem auf dem Server gerendert; der Planer kommt also als leeres <div> an, und das Hüllen-Muster oben gilt unverändert. Um das Server-Rendering dieser Komponente ganz zu überspringen, laden Sie sie mit next/dynamic und { ssr: false, loading: () => <Shell /> } innerhalb einer Client Component; der App Router akzeptiert ssr: false nicht in Server Components. Im Pages Router funktioniert next/dynamic mit ssr: false direkt in einer Seite. Importieren Sie das Stylesheet im Root-Layout (App Router) oder in pages/_app (Pages Router).

React Router und Remix

Routenmodule rendern im SSR-Modus auf dem Server und beim Vorrendern zur Build-Zeit; verwenden Sie daher das oben gezeigte Client-Flag und den Lazy Import innerhalb der Routenkomponente. Die Daten für die Hülle können aus dem Loader der Route kommen, wodurch Server- und Client-Rendering identisch bleiben. Importieren Sie das Stylesheet aus der Root-Route oder Ihrem globalen CSS.

Andere Frameworks

Die Regel ist überall dieselbe: Rendern Sie einen Platzhalter mit fester Größe überall dort, wo das Framework auf dem Server rendert, und mounten Sie die Komponente nur im Browser, zum Beispiel als reine Client-Insel.

Hydration

Die Bibliothek selbst erzeugt keine Hydration-Abweichungen. Abweichungen kommen meist aus der Hülle:

  • Berechnen Sie während des Renderns keine Datumswerte aus der Uhr. Server und Browser können sich über „heute“ uneinig sein, und auch über die Zeitzone. Übergeben Sie die Datumswerte aus Ihrem Loader oder berechnen Sie sie nach dem Mount.
  • Formatieren Sie Datumsangaben in der Hülle mit einer expliziten Locale, wie es toString('d MMM', 'en-us') oben tut, und nie mit den Voreinstellungen des Geräts.
  • Unter StrictMode mountet die Entwicklungsumgebung Komponenten zweimal; die Komponente erzeugt bei jedem Mount ein frisches Control und gibt das vorherige frei.

Content Security Policy

SuperScheduler funktioniert unter einer strengen Policy:

  • Skripte. Keine Inline-Skripte, kein eval und kein new Function. Der Code wird als Module aus Ihrem Bundle geladen, einschließlich der Chunks, die Pro bei Bedarf mit dynamischem import() nachlädt (Tastaturunterstützung, Menüs, Bubbles, Sprachpakete). script-src 'self' oder der Origin, der Ihr Bundle ausliefert, genügt.
  • Styles. Das Stylesheet ist eine normale CSS-Datei, also deckt style-src 'self' es ab. Die Engine positioniert Knoten, indem sie über das CSSOM in element.style schreibt, was style-src nicht einschränkt, und sie fügt keine <style>-Elemente ein. Sie brauchen 'unsafe-inline' für die Bibliothek nicht.
  • Bilder. Das Pro-Stylesheet zeichnet einige kleine Icons und Skeleton-Formen als data:-SVG-Bilder, etwa die Löschschaltfläche von Ereignissen. Erlauben Sie sie mit img-src 'self' data:, sonst erscheinen diese Verzierungen nicht.
txttxt
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.com

Ihr eigenes, auf dem Server gerendertes Markup folgt denselben Regeln: Die style-Prop von React wird im Server-HTML zu einem style-Attribut, das ein strenges style-src vor der Hydration blockiert. Deshalb gibt das Beispiel dem Container seine Größe über eine Klasse.

Checkliste

  • Hülle auf dem Server gerendert, mit der endgültigen Höhe des Planers per CSS reserviert.
  • Planer nur im Browser gemountet, aus einem per Lazy Import geladenen Modul.
  • Stylesheet einmal aus dem Root-Layout oder dem globalen CSS importiert.
  • Keine Ausgaben in Server-Renderings, die von der Uhr oder der Locale des Geräts abhängen.
  • CSP mit script-src 'self', style-src 'self' und img-src 'self' data:; Klassen statt Inline-Styles in Ihren HTML-Strings.

Verwandte Anleitungen: React-Integration, Virtualisierung und Performance, Theming und Fehlerbehebung.