Renderizado en el servidor

Todos los paquetes de reelkit funcionan en el servidor. Impórtalos y renderízalos con Next.js, Remix, Angular SSR, Nuxt 3 o cualquier configuración de SSR.

Cómo funciona

El controlador del slider del core es lógica pura y no accede al DOM al crearse. Los listeners de gestos, los eventos de teclado y las animaciones se conectan solo en los hooks del ciclo de vida del cliente.

Durante el SSR, el Reel de React renderiza un contenedor estático con los slides visibles iniciales (normalmente 3: anterior, actual y siguiente) cuando tiene size, y un contenedor vacío cuando se mide solo. Al hidratarse, conecta los controladores de gestos, teclado y rueda para que todo sea interactivo.

El Reel de Vue renderiza un contenedor vacío en el servidor y dibuja sus slides después de la hidratación. Con un size, ese contenedor no coincide con lo que el cliente renderiza primero, así que en Nuxt va dentro de <ClientOnly> (consulta Nuxt 3).

PaqueteSeguro en SSRNotas
@reelkit/coreSíLógica pura, sin APIs del navegador al importar ni al crear
@reelkit/reactSíReel y ReelIndicator renderizan HTML válido en el servidor
@reelkit/angularSíComponentes standalone, compatibles con SSR en Angular SSR (@angular/ssr)
@reelkit/vueSíSe puede importar y renderizar en el servidor; con un size, en Nuxt 3 pon Reel dentro de <ClientOnly>
@reelkit/react-reel-playerSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/react-lightboxSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/angular-reel-playerSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/angular-lightboxSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/vue-reel-playerSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/vue-lightboxSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/stories-coreSíIndependiente del framework, sin acceso al DOM
@reelkit/react-stories-playerSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/angular-stories-playerSíNo renderiza nada mientras está cerrado (isOpen=false)
@reelkit/vue-stories-playerSíNo renderiza nada mientras está cerrado (isOpen=false)

Next.js App Router

Reel usa eventos y refs del navegador, así que se ejecuta como Client Component. Añade la directiva "use client" al principio del archivo que usa Reel:

tsx
'use client';

import { Reel, ReelIndicator } from '@reelkit/react';

export function Feed({ items }: { items: FeedItem[] }) {
  return (
    <Reel
      count={items.length}
      size={[400, 700]}
      direction="vertical"
      enableWheel
      itemBuilder={(index) => (
        <div className="w-full h-full flex items-center justify-center">
          {items[index].title}
        </div>
      )}
    >
      <ReelIndicator />
    </Reel>
  );
}

Puedes cargar los datos en un Server Component y pasarlos hacia abajo:

tsx
// app/feed/page.tsx (Server Component)
import { Feed } from './Feed';

export default async function FeedPage() {
  const items = await fetchFeedItems();

  return <Feed items={items} />;
}

Next.js Pages Router

Pages Router funciona sin configuración adicional. El componente se renderiza durante el SSR y se hidrata en el cliente:

tsx
// pages/feed.tsx
import { Reel } from '@reelkit/react';
import type { GetServerSideProps } from 'next';

interface Props {
  items: FeedItem[];
}

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const items = await fetchFeedItems();
  return { props: { items } };
};

export default function FeedPage({ items }: Props) {
  return (
    <Reel
      count={items.length}
      size={[400, 700]}
      itemBuilder={(index) => <Slide data={items[index]} />}
    />
  );
}

Tamaño adaptable con SSR

Omite por completo la prop size. Sin size, Reel mide su contenedor en el cliente con ResizeObserver. Durante el SSR el slider renderiza un contenedor vacío; al hidratarse lo mide y renderiza los slides al momento:

tsx
'use client';

import { Reel } from '@reelkit/react';

export function FullScreenFeed({ items }: { items: FeedItem[] }) {
  return (
    <Reel
      count={items.length}
      style={{ width: '100%', height: '100dvh' }}
      itemBuilder={(index) => <Slide data={items[index]} />}
    />
  );
}
Cómo funciona el tamaño automático

Sin size, el tamaño del contenedor lo tiene que dar el CSS (un padre flex o grid, un ancho y alto explícitos o porcentajes). El slider no renderiza nada hasta que termina la primera medición; después ocupa las dimensiones medidas y se adapta solo a los cambios de tamaño posteriores.

Tamaño explícito (manual)

Para controlar el tamaño al píxel, pasa una prop size explícita. Como window.innerWidth no existe durante el SSR, da un valor por defecto y actualízalo al montar:

tsx
'use client';

import { useState, useEffect } from 'react';
import { Reel } from '@reelkit/react';

// Default size for SSR — matches common mobile viewport
const DEFAULT_SIZE: [number, number] = [390, 844];

export function FullScreenFeed({ items }: { items: FeedItem[] }) {
  const [size, setSize] = useState<[number, number]>(DEFAULT_SIZE);

  useEffect(() => {
    const update = () =>
      setSize([window.innerWidth, window.innerHeight]);

    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  return (
    <Reel
      count={items.length}
      size={size}
      itemBuilder={(index) => <Slide data={items[index]} />}
    />
  );
}
Consejo

Elige un tamaño por defecto que coincida con el viewport más habitual (por ejemplo, pensando primero en el móvil). Si el viewport real es distinto, el slider se reajusta al instante al hidratarse.

Componentes de overlay

ReelPlayerOverlay, LightboxOverlay y StoriesOverlay no renderizan nada con isOpen={false}, así que son seguros en SSR por defecto. Solo montan su portal al abrirse, algo que normalmente ocurre tras una interacción del usuario en el cliente:

tsx
'use client';

import { useState } from 'react';
import { ReelPlayerOverlay } from '@reelkit/react-reel-player';

export function VideoFeed({ content }: { content: ContentItem[] }) {
  const [isOpen, setIsOpen] = useState(false);
  const [startIndex, setStartIndex] = useState(0);

  return (
    <>
      <div className="grid grid-cols-3 gap-1">
        {content.map((item, i) => (
          <button
            key={i}
            onClick={() => { setStartIndex(i); setIsOpen(true); }}
          >
            <img src={item.thumbnail} alt="" />
          </button>
        ))}
      </div>

      <ReelPlayerOverlay
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        content={content}
        initialIndex={startIndex}
      />
    </>
  );
}

Angular SSR

Todos los componentes de Angular son seguros en SSR con @angular/ssr. El controlador del slider se crea en ngOnInit, y el trabajo que necesita un navegador, como medir el contenedor, se omite en el servidor mediante isPlatformBrowser. Los componentes overlay no renderizan nada con isOpen=false, así que no generan marcado durante el renderizado en el servidor.

typescript
import { Component, signal } from '@angular/core';
import {
  RkReelPlayerOverlayComponent,
} from '@reelkit/angular-reel-player';

@Component({
  selector: 'app-feed',
  standalone: true,
  imports: [RkReelPlayerOverlayComponent],
  template: `
    <rk-reel-player-overlay
      [isOpen]="isOpen()"
      [content]="content"
      (closed)="isOpen.set(false)"
    />
  `,
})
export class FeedComponent {
  isOpen = signal(false);
  content = [/* ... */];
}

Nuxt 3

Los componentes de ReelKit para Vue se importan y se renderizan en el servidor sin errores. Reel envía un contenedor vacío desde el servidor y dibuja sus slides después de la hidratación. Cuando Reel se mide solo, ese contenedor se hidrata sin discrepancias desde @reelkit/vue 0.9.1; en versiones anteriores se quedaba sin altura y no aparecía ningún slide. Con un size explícito, el primer renderizado del cliente ya trae slides, lo que provoca una discrepancia al hidratar. En ese caso, o en una versión anterior, envuelve Reel en un componente <ClientOnly> o usa el sufijo .client.vue:

vue
<!-- pages/feed.vue -->
<script setup lang="ts">
const items = await useFetch('/api/feed');
</script>

<template>
  <ClientOnly>
    <Feed :items="items.data.value" />
  </ClientOnly>
</template>

El componente Feed usa Reel con normalidad:

vue
<!-- components/Feed.vue -->
<script setup lang="ts">
import { Reel, ReelIndicator } from '@reelkit/vue';

defineProps<{ items: FeedItem[] }>();
</script>

<template>
  <Reel :count="items.length" direction="vertical" enable-wheel>
    <template #item="{ index, size }">
      <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
        {{ items[index].title }}
      </div>
    </template>
    <ReelIndicator />
  </Reel>
</template>

Tamaño adaptable con SSR

Omite la prop size para que el tamaño se mida solo. Reel ocupa el 100 % de su padre y, desde @reelkit/vue 0.9.1, no necesita <ClientOnly>: el servidor envía un contenedor vacío a tamaño completo, y el cliente lo mide y renderiza los slides. El ejemplo mantiene el envoltorio, que también funciona en versiones anteriores:

vue
<template>
  <ClientOnly>
    <div style="width: 100%; height: 100dvh">
      <Reel :count="items.length">
        <template #item="{ index, size }">
          <Slide :data="items[index]" :size="size" />
        </template>
      </Reel>
    </div>
  </ClientOnly>
</template>

Componentes de overlay

ReelPlayerOverlay, LightboxOverlay y StoriesOverlay no renderizan nada cuando isOpen es false, así que no necesitan <ClientOnly> y no generan marcado durante el renderizado en el servidor. Solo teletransportan su contenido al body una vez abiertos en el cliente.

Usar el core directamente

Si usas @reelkit/core directamente para integrarlo con otro framework, puedes crear el controlador en el servidor. Llama a attach() y observe() en el cliente:

typescript
import { createSliderController } from '@reelkit/core';

// Safe to call on the server — no DOM access
const controller = createSliderController({
  count: 10,
  direction: 'vertical',
});

// Only call on the client — attaches DOM event listeners
if (typeof window !== 'undefined') {
  controller.attach(element);
  controller.observe();
}

Resumen

Lo que funciona sin configuración
  • Importar cualquier paquete de reelkit en el servidor
  • Renderizar los componentes del slider durante el SSR (genera HTML estático válido)
  • Crear controladores en el servidor
  • Los componentes overlay con isOpen=false
Qué tener en cuenta
  • Omite size para medir el tamaño de forma automática, o da un valor por defecto si usas dimensiones basadas en el viewport
  • En Nuxt, renderiza el Reel de Vue dentro de <ClientOnly> cuando tenga un size, o con @reelkit/vue anterior a 0.9.1
  • No llames a attach()/observe() en el servidor cuando uses el core directamente