ProducciónSe aplica aLite y Pro
Renderizado en servidor y páginas prerenderizadas
En el servidor, los componentes de Pro y de Lite renderizan un `<div>` vacío; el scheduler se construye en el navegador después de que el componente se monte. Renderiza en el servidor un shell con la misma altura y contenido real, como las próximas reservas, y monta después el scheduler en el cliente, idealmente desde un módulo importado de forma diferida. Los paquetes se pueden importar en el servidor, se hidratan sin discrepancias y no necesitan que tu Content Security Policy permita scripts ni estilos en línea.
SuperScheduler dibuja su cuadrícula con un motor DOM imperativo, y ese motor necesita un navegador: mide el área visible, escucha los eventos de scroll y de puntero, y coloca los nodos a medida que te desplazas. El renderizado en servidor y el prerenderizado estático funcionan bien con él, siempre que decidas qué envía el servidor mientras el navegador construye la cuadrícula real. Esto se aplica a las dos ediciones.
Qué renderiza el servidor
SuperSchedulerComponent, ya venga de super-scheduler, de super-scheduler/react-render o de super-scheduler-lite, renderiza en el servidor un único <div> vacío. El control se crea en componentDidMount, que nunca se ejecuta en el servidor, así que:
- el HTML prerenderizado no contiene filas, eventos ni cabeceras;
ref.current.controlycontrolRefsiguen vacíos hasta que el navegador monta el componente;- importar los paquetes en el servidor es seguro: ningún módulo toca
windownidocumental importarse, tampoco los módulos por subpath de Pro; - la hidratación coincide: el primer render del navegador es el mismo
<div>vacío, y el control lo rellena después de la hidratación.
Entregar un shell útil
Una caja vacía hasta que se ejecuta JavaScript es un primer pintado pobre, y una página vacía para los rastreadores y para quienes leen sin JavaScript. En su lugar, renderiza en el servidor un sustituto:
- Reserva el espacio. Da al contenedor, en CSS, la altura que tendrá el scheduler, para que nada de lo que hay debajo se mueva cuando aparezca la cuadrícula (sin desplazamiento del layout).
- Muestra contenido real. Un título, las etiquetas de la barra de herramientas y una lista breve de las reservas de hoy o de las próximas les dicen a los visitantes y a los buscadores para qué sirve la página. El shell puede usar los mismos datos que el scheduler.
- Márcalo como en carga.
aria-busy="true"en el shell indica a las tecnologías de apoyo que la región todavía se está construyendo. - Deja fuera las partes estáticas. Los títulos, las leyendas y los filtros que no dependen del control se pueden renderizar en el servidor de forma definitiva y se quedan cuando se monta la cuadrícula.
Las páginas de ejemplo de este sitio funcionan así: cada una se prerenderiza con una vista previa estática de la primera vista, y el scheduler interactivo la sustituye cuando el visitante inicia la demo.
Montar en el cliente
Renderiza el shell en el servidor y durante la hidratación, y después cambia al scheduler. useSyncExternalStore con una instantánea de servidor false te da un indicador que vale false en los dos sitios y true justo después de la hidratación, sin discrepancias. Cargar el scheduler con React.lazy mantiene su código fuera del primer bundle; el shell sirve también como fallback de Suspense mientras se descarga el chunk.
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>
)
}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%" llena el elemento anfitrión del componente, que es un <div> sin estilos, así que dale tamaño desde CSS:
.planning {
height: 560px;
}
/* The scheduler's host element and the shell fill the reserved box. */
.planning > div {
height: 100%;
}
.planning__shell {
overflow: auto;
}Deberías ver la lista de estancias en el código fuente de la página y en el primer pintado, y después la cuadrícula en la misma caja un momento después de que la página sea interactiva, sin que se mueva el contenido de debajo.
Importa super-scheduler/styles.css (o super-scheduler-lite/styles.css) una sola vez desde tu layout raíz o tu hoja de estilos global, para que forme parte del CSS que el servidor ya enlaza. Importarlo desde el módulo cargado de forma diferida también funciona cuando tu bundler divide el CSS por chunk.
Notas por framework
Next.js
Ninguno de los dos paquetes marca sus módulos con la directiva 'use client'. En el App Router, renderiza el scheduler desde tu propio Client Component: un archivo que empieza por 'use client' e importa SuperSchedulerComponent. Los Client Components también se renderizan en el servidor, así que el scheduler llega como un <div> vacío y el patrón de shell anterior se aplica sin cambios. Para no renderizar ese componente en el servidor en absoluto, cárgalo con next/dynamic y { ssr: false, loading: () => <Shell /> } desde dentro de un Client Component; el App Router no acepta ssr: false en los Server Components. En el Pages Router, next/dynamic con ssr: false funciona directamente en una página. Importa la hoja de estilos en el layout raíz (App Router) o en pages/_app (Pages Router).
React Router y Remix
Los módulos de ruta se renderizan en el servidor en modo SSR y durante el build cuando prerenderizas, así que usa dentro del componente de ruta el indicador de cliente y la importación diferida de arriba. Los datos del shell pueden venir del loader de la ruta, lo que mantiene idénticos los renders de servidor y de cliente. Importa la hoja de estilos desde la ruta raíz o desde tu CSS global.
Otros frameworks
La regla es la misma en todas partes: renderiza un marcador de posición con tamaño allí donde el framework renderiza en el servidor, y monta el componente solo en el navegador, por ejemplo como una isla exclusiva del cliente.
Hidratación
La librería en sí no produce discrepancias de hidratación. Las discrepancias suelen venir del shell:
- No calcules fechas a partir del reloj durante el render. El servidor y el navegador pueden no estar de acuerdo sobre qué día es «hoy» ni sobre la zona horaria. Pasa las fechas desde tu loader o calcúlalas después del montaje.
- Formatea las fechas del shell con un locale explícito, como hace arriba
toString('d MMM', 'en-us'), y nunca con los valores por defecto del dispositivo. - Con
StrictMode, en desarrollo los componentes se montan dos veces; el componente crea un control nuevo en cada montaje y libera el anterior.
Content Security Policy
SuperScheduler funciona con una política estricta:
- Scripts. Sin scripts en línea,
evalninew Function. El código se carga como módulos desde tu bundle, incluidos los chunks que Pro carga bajo demanda conimport()dinámico (soporte de teclado, menús, burbujas, paquetes de idioma). Basta conscript-src 'self', o con el origen que sirve tu bundle. - Estilos. La hoja de estilos es un archivo CSS normal, así que
style-src 'self'la cubre. El motor coloca los nodos escribiendo enelement.stylea través del CSSOM, algo questyle-srcno restringe, y no inyecta elementos<style>. No necesitas'unsafe-inline'para la librería. - Imágenes. La hoja de estilos de Pro dibuja algunos iconos pequeños y formas de esqueleto de carga como imágenes SVG
data:, por ejemplo el botón para eliminar un evento. Permítelas conimg-src 'self' data:, o esas decoraciones no aparecerán.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.comTu propio marcado renderizado en servidor sigue las mismas reglas: la prop style de React se convierte en un atributo style en el HTML del servidor, que un style-src estricto bloquea antes de la hidratación. Por eso el ejemplo da tamaño al contenedor con una clase.
Lista de comprobación
- Shell renderizado en el servidor, con la altura final del scheduler reservada en CSS.
- Scheduler montado solo en el navegador, desde un módulo importado de forma diferida.
- Hoja de estilos importada una sola vez desde el layout raíz o el CSS global.
- Nada que dependa del reloj ni del locale del dispositivo en los renders de servidor.
- CSP con
script-src 'self',style-src 'self'eimg-src 'self' data:; clases en lugar de estilos en línea en tus cadenas HTML.
Guías relacionadas: Integración con React, Virtualización y rendimiento, Temas y Solución de problemas.