サーバーサイドレンダリング

reelkit のすべてのパッケージはサーバーで動作します。Next.js、Remix、Angular SSR、Nuxt 3 など、どの SSR 環境でもインポートして描画できます。

仕組み

コアのスライダーコントローラーは純粋なロジックで、生成時に DOM へはアクセスしません。ジェスチャーのリスナー、キーボードイベント、アニメーションは、クライアント側のライフサイクルでだけ取り付けられます。

SSR のあいだ、React の Reel は size があれば最初に表示されるスライド(通常は前・現在・次の 3 枚)を持つ静的なコンテナーを描画し、自分で計測する場合は空のコンテナーを描画します。ハイドレーション時に、ジェスチャー・キーボード・ホイールのコントローラーを取り付けて、すべてを操作可能にします。

Vue の Reel はサーバーで空のコンテナーを描画し、ハイドレーションの後にスライドを描きます。size を渡した場合、そのコンテナーはクライアントが最初に描画する内容と一致しません。そのため Nuxt では <ClientOnly> の中に置きます(Nuxt 3 を参照)。

パッケージSSR で安全備考
@reelkit/coreはい純粋なロジックで、インポート時にも生成時にもブラウザー API を使いません
@reelkit/reactはいReel と ReelIndicator はサーバーで正しい HTML を出力します
@reelkit/angularはいスタンドアロンコンポーネントで、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 はブラウザーのイベントと ref を使うため、Client Component として動作します。Reel を使うファイルの先頭に "use client" ディレクティブを追加してください:

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]} />}
    />
  );
}
自動計測の仕組み

size を省略した場合、コンテナーのサイズは CSS で決める必要があります(親の flex/grid、明示的な幅と高さ、またはパーセント指定)。最初の計測が終わるまでスライダーは何も描画せず、その後は計測した寸法いっぱいに広がり、以降のリサイズにも自動で追従します。

サイズの明示(手動の方法)

ピクセル単位で制御したい場合は、size プロップを明示的に渡します。SSR のあいだは window.innerWidth が使えないので、デフォルト値を用意してマウント時に更新してください:

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

最も多い画面サイズに合うデフォルト値を選びましょう(まずはモバイルから)。実際の画面が異なっていても、ハイドレーション時にスライダーがすぐ調整し直します。

オーバーレイコンポーネント

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 のコンポーネントはすべて @angular/ssr で 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 が false のとき何も描画しないので、<ClientOnly> は不要で、サーバーでのレンダリング中にマークアップを出力しません。中身を body にテレポートするのは、クライアントで開いたときだけです。

Core を直接使う

独自のフレームワーク連携のために @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();
}

まとめ

そのまま動くもの
  • どの reelkit パッケージもサーバーでインポートできる
  • SSR 中にスライダーコンポーネントを描画できる(正しい静的 HTML を出力)
  • サーバーでコントローラーを作成できる
  • isOpen=false のオーバーレイコンポーネント
注意すること
  • 自動計測には size を省略する。画面サイズに基づく寸法を使う場合はデフォルト値を用意する
  • Nuxt では、size を渡すときや 0.9.1 より前の @reelkit/vue では、Vue の Reel を <ClientOnly> の中で描画する
  • Core を直接使う場合、サーバーでは attach()/observe() を呼ばない