Рендеринг на сервері

Усі пакети reelkit працюють на сервері. Імпортуйте й рендерте їх у Next.js, Remix, Angular SSR, Nuxt 3 чи будь-якій іншій конфігурації із SSR.

Як це працює

Контролер слайдера в ядрі — чиста логіка, яка під час створення не звертається до DOM. Обробники жестів, події клавіатури та анімації підключаються лише в клієнтських етапах життєвого циклу.

Під час SSR компонент Reel для React рендерить статичний контейнер із початково видимими слайдами (зазвичай 3: попередній, поточний, наступний), якщо має size, і порожній контейнер, якщо вимірює себе сам. Під час гідратації він підключає контролери жестів, клавіатури та колеса миші, і все стає інтерактивним.

Reel для Vue рендерить на сервері порожній контейнер і малює слайди після гідратації. Коли задано size, цей контейнер не збігається з першим рендером на клієнті, тож у Nuxt його кладуть у <ClientOnly> (див. Nuxt 3).

ПакетБезпечний для SSRПримітки
@reelkit/coreТакЧиста логіка, жодних браузерних API під час імпорту чи створення
@reelkit/reactТакReel і ReelIndicator рендерять коректний HTML на сервері
@reelkit/angularТакАвтономні компоненти, сумісні з SSR в Angular SSR (@angular/ssr)
@reelkit/vueТакБезпечно імпортувати й рендерити на сервері; якщо задано size, у Nuxt 3 кладіть Reel у <ClientOnly>
@reelkit/react-reel-playerТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/react-lightboxТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/angular-reel-playerТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/angular-lightboxТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/vue-reel-playerТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/vue-lightboxТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/stories-coreТакБез прив’язки до фреймворку, не звертається до DOM
@reelkit/react-stories-playerТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/angular-stories-playerТакНічого не рендерить, поки закритий (isOpen=false)
@reelkit/vue-stories-playerТакНічого не рендерить, поки закритий (isOpen=false)

Next.js App Router

Reel використовує браузерні події та рефи, тож працює як Client Component. Додайте директиву "use client" на початку файлу, який використовує 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>
  );
}

Дані можна завантажити в Server Component і передати вниз:

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 працює без додаткових налаштувань. Компонент рендериться під час SSR і гідратується на клієнті:

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

Реактивний розмір із SSR

Не передавайте пропс size взагалі. Якщо size не задано, Reel сам вимірює свій контейнер через ResizeObserver на клієнті. Під час SSR слайдер рендерить порожній контейнер; на гідратації він вимірює його й одразу рендерить слайди:

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]} />}
    />
  );
}
How auto-size works

Якщо size не задано, розмір контейнера має задавати CSS (батьківський flex або grid, явні width і height чи відсотки). Слайдер нічого не рендерить до першого вимірювання, а потім заповнює виміряні розміри й далі сам реагує на зміни.

Явний розмір (ручний спосіб)

Для контролю до пікселя передайте явний size. Оскільки window.innerWidth недоступний під час SSR, задайте значення за замовчуванням і оновіть його після монтування:

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

Оберіть розмір за замовчуванням під найпоширенішу область перегляду (наприклад, спершу мобільну). Якщо реальна область перегляду інша, слайдер миттєво підлаштується на гідратації.

Оверлейні компоненти

ReelPlayerOverlay, LightboxOverlay та StoriesOverlay нічого не рендерять, поки isOpen={false}, тож за замовчуванням безпечні для SSR. Свій портал вони монтують лише коли відкриваються — зазвичай після дії користувача на клієнті:

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

Усі компоненти Angular безпечні для SSR з @angular/ssr. Контролер слайдера створюється в ngOnInit, а робота, якій потрібен браузер, як-от вимірювання контейнера, на сервері пропускається через isPlatformBrowser. Оверлейні компоненти нічого не рендерять, поки isOpen=false, тож під час рендерингу на сервері не дають жодної розмітки.

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

Компоненти ReelKit для Vue імпортуються й рендеряться на сервері без помилок. Reel надсилає із сервера порожній контейнер і малює слайди після гідратації. Коли Reel вимірює себе сам, цей контейнер гідратується без розбіжностей починаючи з @reelkit/vue 0.9.1; у попередніх версіях він лишався без висоти, тож слайди не з’являлися. Із явним size перший рендер на клієнті вже містить слайди, і це дає розбіжність під час гідратації. У такому разі або на попередній версії загорніть Reel у <ClientOnly> або дайте файлу суфікс .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>

Компонент Feed використовує Reel як завжди:

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>

Реактивний розмір із SSR

Не передавайте пропс size, щоб увімкнути автоматичне вимірювання. Reel займає 100% розміру батька і, починаючи з @reelkit/vue 0.9.1, не потребує <ClientOnly>: сервер надсилає порожній контейнер на повний розмір, а клієнт вимірює його й рендерить слайди. Приклад зберігає обгортку, бо вона працює й на попередніх версіях:

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>

Оверлейні компоненти

ReelPlayerOverlay, LightboxOverlay та StoriesOverlay нічого не рендерять, поки isOpen хибне, тож їм не потрібен <ClientOnly> і під час рендерингу на сервері вони не дають жодної розмітки. Свій вміст вони телепортують у body лише після відкриття на клієнті.

Пряме використання ядра

Якщо ви використовуєте @reelkit/core напряму для власної інтеграції з фреймворком, контролер можна створити на сервері. Викликайте attach() та observe() на клієнті:

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

Підсумок

What works out of the box
  • Імпорт будь-якого пакета reelkit на сервері
  • Рендеринг компонентів слайдера під час SSR (дає коректний статичний HTML)
  • Створення контролерів на сервері
  • Оверлейні компоненти, поки isOpen=false
What to keep in mind
  • Не передавайте size для автоматичного вимірювання або задайте значення за замовчуванням, якщо розміри залежать від області перегляду
  • У Nuxt рендерте Reel для Vue всередині <ClientOnly>, коли задано size, або на @reelkit/vue до 0.9.1
  • Не викликайте attach()/observe() на сервері, коли працюєте з ядром напряму