React ガイド

@reelkit/react でスライダーを作る方法を説明します。

タッチ優先
慣性とスナップのあるスワイプ
キーボード操作
矢印キーと Escape
ホイールスクロール
任意、デバウンス付き
仮想化
10,000 件以上、DOM には 3 つ
インジケーター
Instagram 風のドットのスクロール
プログラムからの操作
ref で next()、prev()、goTo()
ループモード
終わりのない循環ナビゲーション
方向を選べる
縦でも横でも
再レンダリングなし
シグナルによる状態の更新

Reel コンポーネント

Reel コンポーネントはメインのコンテナーです。スライダーの状態を管理し、タッチジェスチャー、キーボード操作、アニメーションを処理します。

DOM に置かれるスライドは常に 3 枚、現在のスライドとその前後だけなので、1 万件のリストも 10 件のリストと同じ負荷で済みます。無限フィードやプレーヤーを開くグリッドなど、機能全体での使い方はレシピを参照してください。

tsx
import { Reel, ReelIndicator } from '@reelkit/react';

<Reel
  count={items.length}
  size={[width, height]}
  direction="vertical"
  enableWheel
  afterChange={(index) => console.log('Current:', index)}
  itemBuilder={(index, indexInRange, size) => (
    <div style={{ width: size[0], height: size[1] }}>
      Slide {index}
    </div>
  )}
>
  {/* Optional children like ReelIndicator */}
</Reel>

サイズの自動計測

size プロップは任意です。省略すると、Reel は ResizeObserver でコンテナーを自動計測し、CSS で決まるレイアウトに合わせます。コンテナーのサイズは親要素で決める必要があります(flex、grid、または CSS での明示的な寸法など)。

tsx
// Explicit size (fixed)
<Reel count={items.length} size={[400, 600]} itemBuilder={...} />

// Auto-size (responsive — sized by CSS)
<Reel count={items.length} style={{ width: '100%', height: '100dvh' }} itemBuilder={...} />

itemBuilder パターン

itemBuilder プロップは、インデックスを受け取って各スライドの内容を返す関数です。このパターンによって仮想化が可能になり、表示中のアイテムだけが描画されます。

tsx
itemBuilder={(index, indexInRange, size) => {
  // index: actual item index (0 to count-1)
  // indexInRange: position in visible window (0, 1, or 2)
  // size: [width, height] of the container
  return <Slide index={index} />;
}}

組み込みの操作方法:

  • タッチ/スワイプ: ドラッグで移動し、慣性とスナップが効きます
  • キーボード: 矢印キーと Escape
  • マウスホイール: enableWheel プロップで有効にします
  • プログラムから: apiRef で next()、prev()、goTo() を呼びます
tsx
import { useRef } from 'react';
import { Reel, type ReelApi } from '@reelkit/react';

function App() {
  const apiRef = useRef<ReelApi>(null);

  return (
    <>
      <Reel
        count={10}
        size={[400, 600]}
        apiRef={apiRef}
        itemBuilder={(index) => <Slide index={index} />}
      />
      <button onClick={() => apiRef.current?.prev()}>Prev</button>
      <button onClick={() => apiRef.current?.next()}>Next</button>
      <button onClick={() => apiRef.current?.goTo(5)}>Go to 5</button>
    </>
  );
}

URL の状態

useOverlayUrlState はオーバーレイ用の URL 状態コントローラーを作り、そのまま返します。それを *UrlOverlay の controller プロップに渡します。開閉状態は URL が持つので、結び付けられたオーバーレイは自分で開き、開く操作は通常リンクになります。存在しないパラメーターへの最初の書き込みは履歴エントリーを 1 つ積み、以降の書き込みはそれを置き換えるので、ページを送っても戻るボタンが埋もれることはありません。コントローラーは手元に残しておき、value/position を読んだり、プログラムから操作したりするのに使います。set(position) で開き、set(null) で閉じます。set は、オーバーレイがスライドの切り替え時に内部で使っているのと同じ低レベルの書き込みです。

tsx
import { useOverlayUrlState, urlIndexKey } from '@reelkit/react';
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';
import { LightboxUrlOverlay } from '@reelkit/react-lightbox';
import { Link } from 'react-router-dom';

const photo = useOverlayUrlState({
  param: 'photo',
  ...urlIndexKey(() => images.length),
});

// Opening is a link — the overlay reads the URL and opens itself.
<Link to="?photo=3"><img src={images[3].src} /></Link>
<LightboxUrlOverlay controller={photo} images={images} />

// Read the url-derived state, or close programmatically (a low-level write).
photo.position.value; // 3 for ?photo=3, null when nothing is open
photo.set(null); // close

// Routed app: pass a router-backed adapter, otherwise the router's
// own location goes stale and its next navigation drops the param.
const adapter = useReactRouterUrlAdapter();
const routed = useOverlayUrlState({
  param: 'photo',
  adapter,
  ...urlIndexKey(() => images.length),
});

オプションのオブジェクトは param、codec、locator(3 つとも必須)と、任意の adapter を受け取ります。param と adapter は最初の描画で読まれ、その後は固定です(変えるには再マウントします)。一方 codec と locator は毎回読まれるので、リストが伸びたり並べ替えられたりしたあとの検索では、現在のリストが見えます。codec と locator は同じ Id を共有する対なので、一緒に扱います。単純な ?photo=3 のギャラリーなら ...urlIndexKey(() => images.length) を展開すれば、両方が一度に返されます。urlIndexKey はパラメーターをスライドのインデックスに対応させ、getter が返す現在の件数で範囲を制限するので、古いまたは範囲外の ?photo=99 は拒否され、指されていないスライドを開く代わりに URL から自動で取り除かれます。数値ではなく getter を渡してください。そうすればページングされたフィードが伸びても範囲が正しく保たれます。内部では createIndexLocator(locator の側)を包み、indexCodec と組み合わせています。ページングされたフィードや識別子で指すギャラリーは、代わりに独自の codec と locator の対を用意します。すべてのオプションの表は React API リファレンス にあります。

ReelIndicator

スライダー内の現在位置を示す、Instagram 風の進捗インジケーターを表示する任意のコンポーネントです。Reel の中に置くと、コンテキストを通じて親の count と active の値に自動で接続するので、状態を手でつなぐ必要はありません。

tsx
import { Reel, ReelIndicator } from '@reelkit/react';

{/* Auto-connect: count and active are inherited from parent Reel */}
<Reel count={10} size={[400, 600]} itemBuilder={...}>
  <ReelIndicator />
</Reel>

{/* Manual usage: pass count and active explicitly (e.g. outside a Reel) */}
<ReelIndicator count={10} active={currentIndex} />

ポイント

  • size プロップ

    任意の [幅, 高さ] のタプルです。省略すると CSS でサイズが決まります

  • itemBuilder

    インデックスを受け取り、スライドの内容を返します

  • apiRef

    ナビゲーションのためにコントローラーのメソッドへアクセスします

  • afterChange

    UI を更新するために現在のインデックスを追跡します

パフォーマンスのヒント

  • データの配列をメモ化する

    アイテムの配列を useMemo で包んでください。描画のたびに配列の参照が新しくなると、count の更新と表示範囲の再計算が起きます。

  • itemBuilder を軽く保つ

    表示範囲が変わるたびに(通常は 3 枚分)実行されます。中で重い計算や副作用を行わないでください。

  • 端に近づいたらデータを読み込む

    afterChange でユーザーが終わりに近づいたことを検知し、スライドが尽きる前に次のバッチを取得します(レシピ 無限フィード を参照)。

  • スクロールするページではホイールを無効にする

    スクロールするレイアウトの中にスライダーを埋め込むときは、ページのスクロールを奪わないよう enableWheel={false} を指定してください。

次のステップ