サーバーサイドレンダリング
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" ディレクティブを追加してください:
'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 で取得して、下に渡せます:
// 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 で描画され、クライアントでハイドレーションされます:
// 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 のあいだは空のコンテナーを描画し、ハイドレーション時に計測してすぐにスライドを描画します:
'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 が使えないので、デフォルト値を用意してマウント時に更新してください:
'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 に安全です。ポータルをマウントするのは開いたとき(通常はクライアントでの操作がきっかけ)だけです:
'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 のとき何も描画しないので、サーバーでのレンダリング中にマークアップを出力しません。
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 サフィックスを使ってください:
<!-- 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 を使います:
<!-- 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> が不要です。サーバーは全体サイズの空のコンテナーを送り、クライアントがそれを計測してスライドを描画します。以前のバージョンでも動くように、例ではラッパーを残しています:
<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() はクライアントで呼んでください:
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()を呼ばない