# Slots de renderizado React y tarjetas emergentes

> Renderiza eventos, filas, cabeceras, celdas y áreas como componentes React con super-scheduler/react-render, añade tarjetas emergentes y mantén fluido el scroll.

Source: https://superscheduler.org/es/docs/react-render-slots/
Reviewed: 2026-10-07

Importa SuperSchedulerComponent desde super-scheduler/react-render en lugar de desde la raíz del paquete y pasa renderEvent, renderRowHeader, renderTimeHeader, renderCorner, renderCell o renderArea; cada una devuelve el contenido React de un tipo de slot. El HTML o el texto de reserva se pinta primero y React lo sustituye en pequeños lotes cuando el navegador está libre, así que el scroll nunca espera a React. Añade eventHover para tener tarjetas emergentes que los usuarios pueden fijar.

SuperScheduler pinta su cuadrícula con su propio código DOM, y eso es lo que mantiene fluido el scroll con miles de filas y eventos. Cuando el contenido de un evento o de una cabecera debe salir de tus componentes React (tu sistema de diseño, iconos, avatares, valores formateados), el punto de entrada `super-scheduler/react-render` monta contenido React en los slots del scheduler sin cederle a React el control de la cuadrícula.

Los slots de renderizado React y las tarjetas emergentes necesitan SuperScheduler Pro.

## Cambiar al componente react-render
`super-scheduler/react-render` exporta su propio `SuperSchedulerComponent`. Acepta todas las props del componente principal, expone los mismos `ref.current.control` y `controlRef`, y añade las props `render*`, `eventHover`, `renderOptions` y los handlers `onBefore*DomAdd` / `onBefore*DomRemove`.

El componente de la raíz del paquete también acepta estas props, pero solo avisa una vez (`needs the component from "super-scheduler/react-render"`) y no renderiza nada a partir de ellas. Tener la maquinaria de React en su propio punto de entrada hace que las páginas que no la usan no la carguen.

## Los slots
| Prop | Argumentos | Sustituye |
|---|---|---|
| `renderEvent` | `control`, `e`, `data`, `row`, `width`, `lod` | El contenido de la caja de un evento |
| `renderRowHeader` | `control`, `row`, `column` | El contenido de una celda de la cabecera de fila (`column` es el índice de la columna, 0 si no hay columnas) |
| `renderTimeHeader` | `control`, `header` (`start`, `end`, `level`) | El contenido de una celda de la cabecera de tiempo |
| `renderCorner` | `control` | La esquina superior izquierda |
| `renderCell` | `control`, `cell` | El contenido de una celda de la cuadrícula |
| `renderArea` | `control`, `area`, `source` | Un área declarada con `render: true` |

El motor conserva las partes que le pertenecen: la caja del evento y su posición, la barra de duración, los tiradores de redimensionado, las áreas normales, el botón de plegar del árbol y las líneas de la cuadrícula. Un slot es el contenido de dentro.

## Renderizar el contenido de los eventos
```tsx
// src/CampaignBoard.tsx
import { memo, useMemo } from 'react'
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
import 'super-scheduler/styles.css'

type Campaign = { client: string; channel: 'social' | 'print' | 'video'; progress: number }

const CHANNEL_LABEL: Record<Campaign['channel'], string> = {
  social: 'Social',
  print: 'Print',
  video: 'Video',
}

const CampaignContent = memo(function CampaignContent(props: {
  title: string
  campaign: Campaign
  compact: boolean
}) {
  const { title, campaign, compact } = props
  if (compact) return <strong className="campaign__title">{title}</strong>
  return (
    <span className="campaign">
      <strong className="campaign__title">{title}</strong>
      <span className="campaign__meta">
        {campaign.client} · {CHANNEL_LABEL[campaign.channel]} ·{' '}
        {Math.round(campaign.progress * 100)}%
      </span>
    </span>
  )
})

// Module-level functions keep their identity: a new function re-renders every slot.
const renderEvent: NonNullable<SchedulerProps['renderEvent']> = ({ e, data, width, lod }) => {
  // `data` is the event after onBeforeEventRender; custom fields need a cast.
  const campaign = data as SuperScheduler.EventRenderData<Campaign>
  // `width` comes in 8 px steps and changes only when a gesture ends.
  return (
    <CampaignContent title={e.text()} campaign={campaign} compact={width < 160 || lod !== 'full'} />
  )
}

// The HTML fallback paints first and stays if the React content fails.
const onBeforeEventRender: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
  args.data.html = SuperScheduler.Util.escapeHtml(args.data.text)
}

export function CampaignBoard(props: {
  resources: SuperScheduler.ResourceData[]
  campaigns: SuperScheduler.EventData<Campaign>[]
}) {
  const events = useMemo(() => props.campaigns.slice(), [props.campaigns])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={61}
      scale="Day"
      cellWidth={36}
      eventHeight={44}
      resources={props.resources}
      events={events}
      onBeforeEventRender={onBeforeEventRender}
      renderEvent={renderEvent}
    />
  )
}
```
Deberías ver cada campaña con su cliente, su canal y su progreso, y solo el título cuando el evento mide menos de 160 px o el zoom está alejado.

Los argumentos, en detalle:

- `e` es el envoltorio del evento: `e.id()`, `e.text()`, `e.start()`, `e.end()`, y `e.data` para el objeto almacenado.
- `data` es el evento tal como lo dejó `onBeforeEventRender`, con `start` y `end` como valores `SuperScheduler.Date`. Los campos propios necesitan un cast, como en el snippet.
- `width` es el ancho renderizado en pasos de 8 px, que se actualiza al terminar un gesto y no en cada fotograma.
- `lod` es el nivel de detalle (`'full'`, `'compact'` u `'overview'`) en el momento en que se renderizó el contenido.

> **Behavior:**
> El contenido React es visual. El nombre accesible del evento sigue saliendo de `ariaLabel`, `text` o `html` (consulta [teclado y accesibilidad](https://superscheduler.org/es/docs/keyboard-accessibility-touch/#focus-model)), así que mantén un `text` con sentido. Las pulsaciones del puntero dentro de un evento inician la gestión de clic y arrastre del propio evento: no pongas botones ni enlaces en el contenido del evento y lleva las acciones a una tarjeta emergente, un menú contextual o un panel de detalle.

## Cabeceras, esquina, celdas y áreas
```tsx
// src/TeamBoard.tsx
import { useMemo } from 'react'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

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

// Scheduler dates are civil values: format their native Date in UTC to keep the wall clock.
const WEEKDAY = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' })

// Every slot of a kind gets the function's result: return content for each case
// (a null result leaves that slot empty rather than showing the fallback).
const SLOTS: SchedulerProps = {
  renderRowHeader: ({ row }) => {
    const role = typeof row.data.role === 'string' ? row.data.role : ''
    return (
      <span className="person">
        <span className="person__initials" aria-hidden="true">
          {row.name.slice(0, 1)}
        </span>
        <span className="person__name">{row.name}</span>
        {role !== '' && <span className="person__role">{role}</span>}
      </span>
    )
  },
  renderTimeHeader: ({ header }) =>
    header.level === 0 ? (
      <span>{header.start.toString('MMMM yyyy')}</span>
    ) : (
      <span className="day">
        <small>{WEEKDAY.format(header.start.toDate())}</small> {header.start.toString('d')}
      </span>
    ),
  renderCorner: () => <span className="corner">Team</span>,
  // Only areas declared with `render: true` reach renderArea.
  renderArea: ({ area }) =>
    area.id === 'approval' ? <span className="badge">Needs approval</span> : null,
  onBeforeEventRender: (args) => {
    if (args.data.status === 'draft') {
      args.data.areas = [
        { id: 'approval', render: true, right: 4, top: 4, width: 96, height: 16, action: 'None' },
      ]
    }
  },
}

export function TeamBoard(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      {...SLOTS}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      timeHeaders={TIME_HEADERS}
      rowHeaderWidth={200}
      resources={props.resources}
      events={events}
      // Keeps React work bounded on large boards (defaults shown).
      renderOptions={{ sliceMs: 8 }}
    />
  )
}
```
Una función de render es dueña de todos los slots de su tipo. Devuelve contenido para cada caso: un resultado `null` deja ese slot vacío en lugar de mostrar el contenido de reserva. En la práctica, `renderArea` es la excepción, porque solo le llegan las áreas que declaraste con `render: true`.

Notas por slot:

- **Cabeceras de fila.** El botón de plegar del árbol se queda en su sitio. Con `rowHeaderColumns`, la función se ejecuta una vez por columna y recibe su índice en `column`.
- **Cabeceras de tiempo.** `header.level` es el índice en `timeHeaders` (0 es la fila superior). Las fechas del scheduler son valores civiles: para formatearlas con `Intl`, pasa `date.toDate()` y `timeZone: 'UTC'`, como hace el snippet.
- **Celdas.** `renderCell` monta una raíz de React por cada celda montada, y ninguna mientras las celdas miden menos de 24 px. Una vista de 40 filas por 30 días ya monta 1200: para disponibilidad, precios o sombreado, usa en su lugar `html`, `cssClass` o `backColor` en `onBeforeCellRender`.
- **Áreas.** Declara el área en el evento (o en la fila, la celda o la cabecera) con `render: true` y su posición; `source` te dice a qué elemento pertenece el área.

## Contenido de reserva, lotes y ciclo de vida
El contenido React nunca bloquea el pintado:

1. El scheduler pinta primero el HTML o el texto de reserva: el `html` o el `text` que producen tus datos y tus hooks `onBefore*Render`.
2. Cuando el navegador está libre, el contenido React se confirma en lotes que apuntan a `renderOptions.sliceMs` (8 ms por defecto). Cada slot oculta su contenido de reserva en cuanto su contenido está listo.
3. Durante el scroll, el zoom y el arrastre, el contenido existente se mueve con la cuadrícula. Los slots nuevos y los cambios de renderer esperan a que termine el gesto.
4. Si una función de render lanza un error, ese slot conserva su contenido de reserva y el error se comunica una vez por slot mediante `reportError` del navegador (un evento global `error` que tu herramienta de seguimiento de errores puede capturar).

El contenido que sale de la vista con el scroll se conserva desconectado para que pueda volver sin renderizarse de nuevo: hasta `renderOptions.retain` elementos, por defecto el doble de los montados con un máximo de 2000. El estado local de un elemento conservado sobrevive; un elemento descartado empieza de cero. `retain: 0` desactiva la conservación.

El contenido de los slots se renderiza mediante portales, así que ve tus providers: tema, traducciones, router, clientes de datos. El CSS puede apuntar a `[data-super-scheduler-slot]`, `[data-super-scheduler-slot-ready]` y `[data-super-scheduler-fallback]`.

En el servidor, el componente renderiza un `<div>` vacío; los slots aparecen después de que el scheduler se monte en el cliente. Consulta [renderizado en servidor y prerenderizado](https://superscheduler.org/es/docs/ssr-prerender/).

> **Tip:**
> El código migrado de schedulers basados en callbacks puede usar `onBeforeEventDomAdd` y sus equivalentes (celda, cabecera de fila, cabecera de tiempo, esquina): asigna a `args.element` un nodo DOM o un elemento React, y el handler `DomRemove` correspondiente recibe ese mismo elemento. Cuando existen ambos, gana la prop `render*` y se registra un aviso.

## Tarjetas emergentes
`eventHover` muestra una tarjeta React junto a un evento cuando el puntero se detiene sobre él. Sin `eventHover`, no aparece ninguna tarjeta.

| Opción | Por defecto | Efecto |
|---|---|---|
| `render(args)` | obligatoria | Contenido de la tarjeta; `args` tiene `control`, `e`, `row`, `anchor` (la caja del evento), `pinned` y `close()` |
| `delay` | `350` | Milisegundos que el puntero debe estar quieto antes de que se abra la tarjeta |
| `leaveGrace` | `180` | Milisegundos antes de cerrarla cuando el puntero sale del evento o de la tarjeta |
| `placement` | `'auto'` | `'auto'`, `'above'`, `'below'`, `'start'` o `'end'` |
| `pin` | `false` | `'click'` o `'dblclick'` fija la tarjeta para que los usuarios puedan interactuar con ella |
| `glide` | `true` | Pasar a otro evento desplaza la tarjeta abierta en lugar de volver a abrirla |

```tsx
// src/BookingsWithCards.tsx
import { useMemo } from 'react'
import type { SchedulerEventHoverOptions, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

// Module-level: the options object keeps its identity across renders.
const BOOKING_CARD: SchedulerEventHoverOptions = {
  delay: 350,
  leaveGrace: 180,
  placement: 'auto',
  // A click pins the card as a non-modal dialog; on touch screens a tap does it.
  pin: 'click',
  render: ({ e, row, pinned, close }) => (
    <article className="booking-card">
      <h3>{e.text()}</h3>
      <p>{row.name}</p>
      <p>
        {e.start().toString('d MMM, HH:mm')} to {e.end().toString('d MMM, HH:mm')}
      </p>
      {pinned && (
        <button type="button" onClick={close}>
          Close
        </button>
      )}
    </article>
  ),
}

export function BookingsWithCards(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={14}
      scale="Day"
      resources={props.resources}
      events={events}
      eventHover={BOOKING_CARD}
    />
  )
}
```
Cómo se comporta la tarjeta:

- Una tarjeta sin fijar es un `role="tooltip"`; una tarjeta fijada es un `role="dialog"` no modal que toma el foco. Esc o un clic fuera cierra una tarjeta fijada y devuelve el foco al evento.
- Llevar el puntero a la tarjeta la mantiene abierta. El scroll, el zoom, el arrastre y la selección la ocultan de inmediato.
- La tarjeta se coloca al abrirse, se voltea o se encoge para caber en la ventana y respeta la preferencia de movimiento reducido.
- Vive en `document.body` y lleva el tema del scheduler. Dale estilo con `--super-scheduler-hover-padding`, `-hover-border`, `-hover-radius`, `-hover-bg`, `-hover-color`, `-hover-shadow` y `--super-scheduler-z-hover`.
- Las pantallas táctiles no tienen hover: con `pin: 'click'`, un toque abre una tarjeta fijada.

Las tarjetas emergentes son independientes de las burbujas HTML (`bubble`, `bubbleHtml`). Las burbujas no pueden contener contenido React; para eso, usa `eventHover`.

## Rendimiento
- **Funciones estables.** Define las funciones de render y los objetos de opciones a nivel de módulo, o memorízalos. Una función con identidad nueva vuelve a renderizar todos los slots de ese tipo.
- **Renders baratos.** `sliceMs` es un objetivo para los lotes, no un límite para tu código: una función de render lenta retrasa su lote. No leas el layout ni midas el DOM dentro de las funciones de render; usa `width` y `lod`.
- **Componentes memorizados.** Envuelve los componentes de los slots en `memo` y pásales props primitivas, como en el snippet de eventos.
- **Contexto.** Un valor de contexto que cambia a menudo vuelve a renderizar todos los slots que lo leen. Mantén el estado que cambia rápido (posición del puntero, temporizadores) fuera de los contextos que consumen los slots.
- **Celdas.** En cuadrículas grandes, usa preferentemente las cadenas de `onBeforeCellRender` en lugar de `renderCell`.
- **Nada de estado por fotograma.** No actualices el estado de React desde `onScroll` ni desde los handlers de arrastre; la librería hace su trabajo de cada fotograma sin renders de React.

Mide tu propio contenido con el React Profiler: la librería no puede abaratar un componente caro.

## Relacionado
→ https://superscheduler.org/es/examples/agency-campaigns/
→ https://superscheduler.org/es/examples/lab-instruments/
- [Temas, tokens, Tailwind y modo oscuro](https://superscheduler.org/es/docs/theming/) para dar estilo al contenido de los slots con los tokens del scheduler.
- [Rendimiento y virtualización](https://superscheduler.org/es/docs/performance-virtualization/) para el modelo de render que hay detrás de los slots.
