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).
| Paquete | Seguro en SSR | Notas |
|---|---|---|
@reelkit/core | Sí | Lógica pura, sin APIs del navegador al importar ni al crear |
@reelkit/react | Sí | Reel y ReelIndicator renderizan HTML válido en el servidor |
@reelkit/angular | Sí | Componentes standalone, compatibles con SSR en Angular SSR (@angular/ssr) |
@reelkit/vue | Sí | Se puede importar y renderizar en el servidor; con un size, en Nuxt 3 pon Reel dentro de <ClientOnly> |
@reelkit/react-reel-player | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/react-lightbox | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/angular-reel-player | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/angular-lightbox | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/vue-reel-player | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/vue-lightbox | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/stories-core | Sí | Independiente del framework, sin acceso al DOM |
@reelkit/react-stories-player | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/angular-stories-player | Sí | No renderiza nada mientras está cerrado (isOpen=false) |
@reelkit/vue-stories-player | Sí | 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:
'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:
// 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:
// 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:
'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:
'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:
'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.
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:
<!-- 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:
<!-- 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:
<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:
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
sizepara 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
Reelde Vue dentro de<ClientOnly>cuando tenga unsize, o con@reelkit/vueanterior a 0.9.1 - No llames a
attach()/observe()en el servidor cuando uses el core directamente