Rendern auf dem Server

Alle Pakete von reelkit laufen auf dem Server. Du kannst sie mit Next.js, Remix, Angular SSR, Nuxt 3 oder jedem anderen SSR-Setup importieren und rendern.

So funktioniert es

Der Slider-Controller im Core ist reine Logik und greift beim Erstellen nicht auf das DOM zu. Gesten-Listener, Tastatur-Events und Animationen werden erst in den Lifecycle-Hooks auf dem Client angehängt.

Beim SSR rendert Reel für React einen statischen Container mit den anfangs sichtbaren Slides (meist 3: vorheriger, aktueller, nächster), wenn es ein size hat, und einen leeren Container, wenn es sich selbst misst. Bei der Hydration hängt es die Controller für Gesten, Tastatur und Mausrad an und macht damit alles interaktiv.

Reel für Vue rendert auf dem Server einen leeren Container und zeichnet seine Slides erst nach der Hydration. Mit einer size passt dieser Container nicht zu dem, was der Client zuerst rendert. In Nuxt gehört es dann in <ClientOnly> (siehe Nuxt 3).

PaketSSR-sicherHinweise
@reelkit/coreJaReine Logik, keine Browser-APIs beim Import oder Erstellen
@reelkit/reactJaReel und ReelIndicator rendern auf dem Server gültiges HTML
@reelkit/angularJaStandalone-Komponenten, SSR-kompatibel mit Angular SSR (@angular/ssr)
@reelkit/vueJaLässt sich auf dem Server importieren und rendern; mit einer size gehört Reel in Nuxt 3 in <ClientOnly>
@reelkit/react-reel-playerJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/react-lightboxJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/angular-reel-playerJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/angular-lightboxJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/vue-reel-playerJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/vue-lightboxJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/stories-coreJaUnabhängig vom Framework, kein Zugriff auf das DOM
@reelkit/react-stories-playerJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/angular-stories-playerJaRendert im geschlossenen Zustand nichts (isOpen=false)
@reelkit/vue-stories-playerJaRendert im geschlossenen Zustand nichts (isOpen=false)

Next.js App Router

Reel nutzt Browser-Events und Refs und läuft deshalb als Client Component. Setze die Direktive "use client" an den Anfang der Datei, die Reel verwendet:

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>
  );
}

Die Daten kannst du in einer Server Component laden und nach unten weitergeben:

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

Der Pages Router funktioniert ohne zusätzliche Konfiguration. Die Komponente wird beim SSR gerendert und auf dem Client hydriert:

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]} />}
    />
  );
}

Responsive Größe mit SSR

Lass das Prop size ganz weg. Ohne size misst Reel seinen Container auf dem Client selbst über ResizeObserver. Beim SSR rendert der Slider einen leeren Container; bei der Hydration misst er und rendert die Slides sofort:

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]} />}
    />
  );
}
So funktioniert die automatische Größe

Ohne size muss die Größe des Containers per CSS festgelegt sein (Flex- oder Grid-Elternelement, feste Breite und Höhe oder Prozentwerte). Der Slider rendert nichts, bis die erste Messung fertig ist, füllt danach die gemessene Fläche und reagiert auch auf spätere Größenänderungen automatisch.

Feste Größe (manueller Weg)

Für Kontrolle auf den Pixel genau übergibst du ein festes Prop size. Da window.innerWidth beim SSR nicht verfügbar ist, gib einen Standardwert vor und aktualisiere ihn beim Mounten:

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]} />}
    />
  );
}
Tipp

Wähle als Standard die Größe, die zu deinem häufigsten Viewport passt (zum Beispiel Mobile First). Weicht der tatsächliche Viewport ab, passt sich der Slider bei der Hydration sofort an.

Overlay-Komponenten

ReelPlayerOverlay, LightboxOverlay und StoriesOverlay rendern bei isOpen={false} nichts und sind damit von Haus aus SSR-sicher. Ihr Portal mounten sie erst beim Öffnen (meist nach einer Interaktion auf dem Client):

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

Alle Angular-Komponenten sind mit @angular/ssr SSR-sicher. Der Slider-Controller entsteht in ngOnInit, und die Arbeit, die einen Browser braucht, etwa das Messen des Containers, wird auf dem Server über isPlatformBrowser übersprungen. Overlay-Komponenten rendern bei isOpen=false nichts und erzeugen beim Server-Rendering daher kein Markup.

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

Die Vue-Komponenten von ReelKit lassen sich ohne Fehler auf dem Server importieren und rendern. Reel schickt vom Server einen leeren Container und zeichnet seine Slides erst nach der Hydration. Wenn sich Reel selbst misst, wird dieser Container ab @reelkit/vue 0.9.1 ohne Abweichung hydriert; in früheren Versionen hatte er keine Höhe, sodass keine Slides erschienen. Mit einer festen size enthält schon der erste Render auf dem Client Slides, und das ergibt eine Abweichung bei der Hydration. In diesem Fall oder mit einer früheren Version umschließe Reel mit einer Komponente <ClientOnly> oder verwende die Endung .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>

Die Komponente Feed verwendet Reel ganz normal:

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>

Responsive Größe mit SSR

Lass das Prop size weg, um die automatische Messung zu nutzen. Reel nimmt dann 100 % seines Elternelements ein und braucht ab @reelkit/vue 0.9.1 kein <ClientOnly>: Der Server schickt einen leeren Container in voller Größe, und der Client misst ihn und rendert die Slides. Das Beispiel behält die Hülle, weil sie auch mit früheren Versionen funktioniert:

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>

Overlay-Komponenten

ReelPlayerOverlay, LightboxOverlay und StoriesOverlay rendern nichts, solange isOpen false ist. Sie brauchen daher kein <ClientOnly> und erzeugen beim Server-Rendering kein Markup. Ihren Inhalt teleportieren sie erst in den Body, wenn sie auf dem Client geöffnet werden.

Den Core direkt nutzen

Wenn du @reelkit/core direkt für eine eigene Framework-Integration nutzt, kannst du den Controller auf dem Server erstellen. attach() und observe() rufst du auf dem Client auf:

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();
}

Zusammenfassung

Was ohne weitere Einrichtung funktioniert
  • Jedes Paket von reelkit auf dem Server importieren
  • Slider-Komponenten beim SSR rendern (ergibt gültiges statisches HTML)
  • Controller auf dem Server erstellen
  • Overlay-Komponenten bei isOpen=false
Worauf du achten solltest
  • Lass size für die automatische Messung weg oder gib einen Standardwert vor, wenn die Maße vom Viewport abhängen
  • Rendere Reel für Vue in Nuxt innerhalb von <ClientOnly>, wenn es eine size hat oder du @reelkit/vue vor 0.9.1 nutzt
  • Ruf attach()/observe() nicht auf dem Server auf, wenn du den Core direkt nutzt