Renderização no servidor
Todos os pacotes do reelkit funcionam no servidor. Importe e renderize com Next.js, Remix, Angular SSR, Nuxt 3 ou qualquer configuração de SSR.
Como funciona
O controlador do slider é lógica pura, sem nenhum acesso ao DOM na construção. Escutas de gesto, eventos de teclado e animações só entram nos ciclos de vida do lado do cliente.
Durante o SSR, o Reel do React renderiza um contêiner estático com os slides visíveis iniciais (normalmente 3: anterior, atual, próximo) quando recebe size, e um contêiner vazio quando mede a si mesmo. Na hidratação, ele conecta os controladores de gesto, teclado e roda do mouse, deixando tudo interativo.
O Reel do Vue renderiza um contêiner vazio no servidor e desenha os slides depois da hidratação. Com um size, esse contêiner não bate com a primeira renderização no cliente, então no Nuxt ele vai dentro de <ClientOnly> (veja Nuxt 3).
| Pacote | Seguro no SSR | Observações |
|---|---|---|
@reelkit/core | Sim | Lógica pura, sem APIs do navegador na importação ou na construção |
@reelkit/react | Sim | Reel e ReelIndicator produzem HTML válido no servidor |
@reelkit/angular | Sim | Componentes standalone, compatíveis com Angular SSR (@angular/ssr) |
@reelkit/vue | Sim | Seguro para importar e renderizar no servidor; com um size, no Nuxt 3 coloque o Reel dentro de <ClientOnly> |
@reelkit/react-reel-player | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/react-lightbox | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/angular-reel-player | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/angular-lightbox | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/vue-reel-player | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/vue-lightbox | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/stories-core | Sim | Independente de framework, sem acesso ao DOM |
@reelkit/react-stories-player | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/angular-stories-player | Sim | Não renderiza nada quando fechado (isOpen=false) |
@reelkit/vue-stories-player | Sim | Não renderiza nada quando fechado (isOpen=false) |
Next.js App Router
O Reel usa eventos do navegador e refs, então ele roda como Client Component. Adicione a diretiva "use client" no topo do arquivo 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>
);
}Você pode buscar os dados em um Server Component e repassá-los:
// 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
O Pages Router funciona sem configuração extra. O componente renderiza no SSR e hidrata no 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]} />}
/>
);
}Tamanho responsivo com SSR
Simplesmente não passe a prop size. Sem ela, o Reel mede o próprio contêiner no cliente via ResizeObserver. Durante o SSR o slider renderiza um contêiner vazio; na hidratação ele mede e exibe os slides na hora:
'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]} />}
/>
);
}Como funciona a medição automática
Quando size não é informado, o contêiner precisa ganhar tamanho pelo CSS (flex/grid do elemento pai, largura/altura explícitas ou porcentagens). O slider não mostra nada até concluir a primeira medição; depois disso preenche as dimensões medidas e acompanha sozinho os redimensionamentos seguintes.
Tamanho explícito (abordagem manual)
Para controle no nível do pixel, passe a prop size explicitamente. Como window.innerWidth não existe durante o SSR, defina um valor padrão e atualize na montagem:
'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]} />}
/>
);
}Dica
Escolha um tamanho padrão próximo da tela mais comum entre seus leitores (comece pelo celular). Se a tela real for diferente, o slider se reajusta na hidratação, sem atraso perceptível.
Componentes de overlay
ReelPlayerOverlay, LightboxOverlay e StoriesOverlay não renderizam nada quando isOpen={false}, então já são seguros no SSR. Eles só montam seu portal ao abrir, o que normalmente parte de uma interação no 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 os componentes Angular são seguros no SSR com @angular/ssr. O controlador do slider é criado em ngOnInit, e o trabalho que precisa do navegador, como medir o contêiner, é pulado no servidor via isPlatformBrowser. Componentes de overlay não renderizam nada quando isOpen=false, então não produzem marcação alguma no 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
Os componentes Vue do ReelKit são importados e renderizados no servidor sem erros. O Reel envia do servidor um contêiner vazio e desenha os slides depois da hidratação. Quando o Reel mede a si mesmo, esse contêiner hidrata sem divergência a partir do @reelkit/vue 0.9.1; nas versões anteriores ele ficava sem altura e nenhum slide aparecia. Com um size explícito, a primeira renderização no cliente já traz slides, o que gera uma divergência na hidratação. Nesse caso, ou numa versão anterior, envolva o Reel em um <ClientOnly> ou use o sufixo .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>O componente Feed usa o Reel normalmente:
<!-- 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>Tamanho responsivo com SSR
Não passe a prop size e deixe a medição automática agir. O Reel ocupa 100% do elemento pai e, a partir do @reelkit/vue 0.9.1, dispensa o <ClientOnly>: o servidor envia um contêiner vazio no tamanho total, e o cliente o mede e exibe os slides. O exemplo mantém o wrapper, que também funciona nas versões 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 e StoriesOverlay não renderizam nada quando isOpen é falso, então não precisam de <ClientOnly> e não produzem marcação alguma no servidor. Eles só teleportam o conteúdo para o body depois de abertos no cliente.
Usando o core diretamente
Ao usar @reelkit/core direto para integrar um framework próprio, você pode criar o controlador no servidor. Chame attach() e observe() apenas no 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();
}Resumo
O que já funciona sem ajustes
- Importar qualquer pacote do reelkit no servidor
- Renderizar os componentes de slider durante o SSR (gera HTML estático válido)
- Criar controladores no servidor
- Componentes de overlay com
isOpen=false
O que exige atenção
- Omita
sizepara medir automaticamente, ou defina um padrão ao usar dimensões baseadas na janela - No Nuxt, renderize o
Reeldo Vue dentro de<ClientOnly>quando ele tiver umsizeou no@reelkit/vueanterior a 0.9.1 - Não chame
attach()/observe()no servidor ao usar o core diretamente