mcpField
I
inertia-rails-pages

inertia-rails-pages

github.com/inertia-rails開発70506 回インストール37 週間前に更新
リポジトリ

inertia-rails-pages について

Inertia.jsのページを実装するエンジニア向けに、Reactのページコンポーネント、レイアウト、ナビゲーション、クライアントAPIを解説します。永続レイアウトやURL由来のUI状態、ページデータの更新・読み込み方法を扱います。

主な機能

  1. 01ページコンポーネントとプロパティ
  2. 02永続レイアウトと状態の保持
  3. 03URLプロパティに基づくUI状態
  4. 04routerによる遷移とクライアント側のプロパティ更新
  5. 05遅延プロパティと表示領域に応じた読み込み

活用シーン

  1. 01永続レイアウトを備えたInertia.jsページの構築
  2. 02URLパラメーターによるダイアログやフィルターの制御
  3. 03遷移せずにページデータの一部を更新
  4. 04遅延データや表示領域のコンテンツを必要時に読み込み

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/inertia-rails/skills --skill inertia-rails-pages

インストール後、Claude Code で/inertia-rails-pagesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
layouts.md
navigation.md
svelte.md
vue.md

詳細

Inertia Rails ページ

ページコンポーネント、レイアウト、ナビゲーション、クライアントサイド API。

ページを作成する前に確認してください:

  • このページにレイアウトは必要ですか? → 永続レイアウトを使用します(React:Page.layout = ...、Vue:defineOptions({ layout })、Svelte:モジュールスクリプトのエクスポート)。JSX/テンプレートでラップすると、ナビゲーションのたびに再マウントされ、スクロール位置、音声再生、コンポーネントの状態が失われます
  • UI の状態は URL から取得しますか? → コントローラー(params を読み取り、プロパティとして渡す)とコンポーネント(プロパティから導出し、useState/useEffect は使わない)の両方を変更します。URL の更新には router.get を使用します
  • ナビゲーションせずにデータを更新する必要がありますか? → router.reload({ only: [...] }) を使用します。useEffect と fetch の組み合わせは使用しないでください
  • サーバーとの往復なしでプロパティを更新する必要がありますか? → router.replaceProp を使用します。フェッチも再読み込みも発生しません

絶対に行わないでください:

  • window.location.search を解析したり、useSearchParams を使用したりしないでください。URL の状態はコントローラーのプロパティから導出します
  • URL と React の状態を同期するために useState/useEffect を使用しないでください。コントローラーが URL から導出したデータをプロパティとして渡すため、コンポーネントはそれを読み取るだけです
  • <Deferred> のレンダー関数に引数を渡さないでください。{(data) => ...} は機能しません。子コンポーネントは usePage() で読み取ります
  • usePage().props.flash にアクセスしないでください。flash はトップレベルにあります:usePage().flash
  • 永続化のために JSX の戻り値でレイアウトをラップしないでください。Page.layout = ... を使用するか、createInertiaApp の resolve コールバック内でグローバルレイアウトを指定します

ページコンポーネントの構造

ページは、コントローラーのプロパティを関数引数として受け取るデフォルトエクスポートです。 type Props = { ... } を使用してください(interface は使用しないでください。React では TS2344 が発生します)。Vue では defineProps<T>()、Svelte では $props() を使った let { ... } = $props() を使用します。

type Props = {
  posts: Post[]
}

export default function Index({ posts }: Props) {
  return <PostList posts={posts} />
}

永続レイアウト

レイアウトはナビゲーションをまたいで維持されるため、再マウントされず、スクロール位置や音声などが保持されます。

import { AppLayout } from '@/layouts/app-layout'

export default function Show({ course }: Props) {
  return <CourseContent course={course} />
}

// Single layout
Show.layout = (page: React.ReactNode) => <AppLayout>{page}</AppLayout>

エントリーポイントでのデフォルトレイアウト:

// app/frontend/entrypoints/inertia.tsx
resolve: async (name) => {
  const page = await pages[`../pages/${name}.tsx`]()
  page.default.layout ??= (page: React.ReactNode) => <AppLayout>{page}</AppLayout> // default if not set
  return page
}

ナビゲーション

<Link> と router

内部ナビゲーションには <a> ではなく <Link href="..."> を使用し、プログラムによるナビゲーションには router.get/post/patch/delete を使用します。特に分かりにくい主な機能は次のとおりです。

// Prefetching — preloads page data on hover
<Link href="/users" prefetch>Users</Link>
<Link href="/users" prefetch cacheFor="30s">Users</Link>

// Prefetch with cache tags — invalidate after mutations
<Link href="/users" prefetch cacheTags="users">Users</Link>

// Programmatic prefetch (e.g., likely next destination)
router.prefetch('/settings', {}, { cacheFor: '1m' })

// Partial reload — refresh specific props without navigation
router.reload({ only: ['users'] })

router API 全体、訪問オプション、イベントコールバックについては references/navigation.md を参照してください。以下の読み込みトリガーもご覧ください。

クライアントサイドのプロパティヘルパー

サーバーとの往復なしでプロパティを更新します:

// Replace a single prop (dot notation supported)
router.replaceProp('show_modal', false)
router.replaceProp('user.name', 'Jane Smith')

// With callback (receives current value + all props)
router.replaceProp('count', (current) => current + 1)

// Append/prepend to array props
router.appendToProp('messages', { id: 4, text: 'New' })
router.prependToProp('notifications', (current, props) => ({
  id: Date.now(),
  message: `Hello ${props.auth.user.name}`,
}))

これらは preserveScroll と preserveState が自動的に true に設定される router.replace() のショートカットです。

router.replaceProp と router.reload の違い: クライアント側のみの状態変更(モーダルの切り替え、カウンターのインクリメント)には router.replaceProp を使用します。サーバーとの往復は発生しません。サーバーから最新のデータ(更新されたレコード、再計算された統計情報)が必要な場合は router.reload を使用します。

URL 駆動の状態(ダイアログ、タブ、フィルター)

URL の状態 = サーバーの状態 = プロパティです。必ず両側を実装してください:

  1. コントローラー — params を読み取り、プロパティとして渡します
  2. コンポーネント — そのプロパティから UI の状態を導出します(useState と useEffect は使いません)
  3. 更新 — クエリパラメーターを指定した router.get で URL を変更します(サーバーとの往復が発生し、新しいプロパティが渡されます)

絶対に useState と useEffect を使って URL とダイアログ/タブ/フィルターの状態を同期しないでください。 サーバーが唯一の信頼できる情報源であり、コンポーネントはプロパティを読み取るだけです。

# Step 1: Controller reads params, passes as prop
def index
  render inertia: {
    users: User.all,
    selected_user_id: params[:user_id]&.to_i
  }
end
// Step 2+3: Derive state from props, router.get to update URL

type Props = {
  users: User[]
  selected_user_id: number | null  // from controller
}

export default function Index({ users, selected_user_id }: Props) {
  // Derive — no useState, no useEffect, no window.location parsing
  const selectedUser = selected_user_id
    ? users.find(u => u.id === selected_user_id)
    : null

  const openDialog = (id: number) =>
    router.get('/users', { user_id: id }, {
      preserveState: true,
      preserveScroll: true,
    })

  const closeDialog = () =>
    router.get('/users', {}, {
      preserveState: true,
      preserveScroll: true,
    })

  return (
    <Dialog open={!!selectedUser} onOpenChange={(open) => !open && closeDialog()}>
      <DialogContent>{/* ... */}</DialogContent>
    </Dialog>
  )
}

なぜ useEffect を使わないのですか? router.get('/users', { user_id: 5 }) が実行されると、Inertia はサーバーにリクエストを送信します → コントローラーが params[:user_id] = 5 の状態で実行されます → selected_user_id: 5 を含む新しいプロパティが返されます → ダイアログが開いた状態でコンポーネントが再レンダーされます。処理の流れは URL → サーバー → プロパティ → レンダーです。クライアント側で window.location を解析するのは、サーバーがすでに行っている処理を重複させることになります。

共有プロパティ

共有プロパティ(認証情報、flash)は InertiaConfig を通じてグローバルに型定義されます(inertia-rails-typescript スキルを参照)。ページコンポーネントでは独自のプロパティのみを型定義します:

type Props = {
  users: User[]         // page-specific only
  // auth is NOT here — typed globally via InertiaConfig
}

export default function Index({ users }: Props) {
  const { props, flash } = usePage()
  // props.auth typed via InertiaConfig, flash.notice typed via InertiaConfig
  return <UserList users={users} />
}

Flash へのアクセス

Flash はページオブジェクトのトップレベルにあり、props の中にはありません。これが flash に関する最もよくある間違いです。Flash の設定については inertia-rails-controllers、トースト UI については shadcn-inertia を参照してください。

// BAD:  usePage().props.flash   ← WRONG, flash is not in props
// GOOD: usePage().flash         ← flash.notice, flash.alert

<Deferred> コンポーネント

遅延プロップが届くまでフォールバックを表示します。子要素には通常の ReactNode または () => ReactNode のレンダー関数を指定できます。どちらの場合も、子要素は usePage() を通じてページプロップから遅延プロップを読み取ります。レンダー関数に渡される引数はありません。

import { Deferred } from '@inertiajs/react'

export default function Dashboard({ basic_stats }: Props) {
  return (
    <>
      <QuickStats data={basic_stats} />
      <Deferred data="detailed_stats" fallback={<Spinner />}>
        <DetailedStats />
      </Deferred>
    </>
  )
}

// Also valid — render function (no args, child still reads from usePage):
// <Deferred data="stats" fallback={<Spinner />}>
//   {() => <Stats />}
// </Deferred>

// BAD — render function does NOT receive data as argument:
// <Deferred data="stats">{(data) => <Stats data={data} />}</Deferred>

<InfiniteScroll> コンポーネント

自動無限スクロールです。ユーザーが下にスクロールすると、次のページを読み込みます。サーバー側の InertiaRails.scroll と組み合わせて使用します(inertia-rails-controllers を参照)。

import { InfiniteScroll } from '@inertiajs/react'

export default function Index({ posts }: Props) {
  return (
    <InfiniteScroll data="posts" loading={() => <Spinner />}>
      {posts.map(post => <PostCard key={post.id} post={post} />)}
    </InfiniteScroll>
  )
}

プロップ:data(プロップ名)、loading(フォールバック)、manual(自動ではなくボタンを表示)、 manualAfter={3}(最初の3ページは自動で読み込み、その後はボタンを表示)、preserveUrl(URLを更新しない)。

<WhenVisible> コンポーネント

要素がビューポートに入ったときにデータを読み込みます。遅延セクション(コメント、 関連アイテムなど)に使用し、無限スクロールには使用しないでください(上記の <InfiniteScroll> を使用します)。

import { WhenVisible } from '@inertiajs/react'

<WhenVisible data="comments" fallback={<Spinner />}>
  <CommentsList />
</WhenVisible>

トラブルシューティング

症状原因修正方法
ナビゲーションのたびにレイアウトが再マウントされるPage.layout ではなく JSX の戻り値でレイアウトをラップしている永続レイアウトを使用します
Deferred の子要素がレンダーされないレンダー関数が引数 {(data) => ...} を受け取る想定になっているレンダー関数に引数は渡されません。{() => <Child />} または通常の <Child /> を使用します。子要素は usePage() を通じてプロップを読み取ります
Flash が undefined になるusePage().props.flash にアクセスしているFlash はトップレベルにあります。props 内ではなく、usePage().flash を使用します
ナビゲーションで URL の状態が失われるuseEffect で window.location を解析しているプロップから導出します。コントローラーで params を読み取り、プロップとして渡します
WhenVisible が起動しない要素がビューポート内にない、またはプロップ名が正しくないdata は、コントローラーが部分再読み込み時に提供するプロップ名と一致させる必要があります
router.get でコンポーネントの状態がリセットされるpreserveState: true がないフィルター、並べ替え、タブの変更では、訪問オプションに preserveState: true を追加します
フォーム送信後にスクロール位置が先頭に戻るpreserveScroll がない訪問オプションまたはフォームオプションに preserveScroll: true を追加します

関連スキル

  • Flash の設定 → inertia-rails-controllers(flash_keys)
  • Flash トースト UI → shadcn-inertia(Sonner + useFlash)
  • 共有プロップの型定義 → inertia-rails-typescript(InertiaConfig)
  • サーバー側の遅延読み込み → inertia-rails-controllers(InertiaRails.defer)
  • URL 連動ダイアログ → shadcn-inertia(Dialog コンポーネント)

Vue / Svelte

上記の例はすべて React 構文を使用しています。Vue 3 または Svelte での同等の実装については、以下を参照してください。

  • Vue 3:references/vue.md — defineProps、usePage() コンポーザブル、<Deferred>/<WhenVisible>/<InfiniteScroll> 用のスコープ付きスロット、永続レイアウト用の defineOptions({ layout })
  • Svelte:references/svelte.md — $props()、$page ストア、<Deferred>/<WhenVisible>/<InfiniteScroll> 用の {#snippet} 構文、<Head> の代わりとなる <svelte:head>、モジュールスクリプトによるレイアウトのエクスポート

必須 — プロジェクトで Vue または Svelte を使用している場合は、対応するファイルを読んでください。 上記の概念と「必ず避ける」ルールはすべてのフレームワークに適用されますが、コード構文は異なります。

参考資料

イベントコールバック(onBefore、 onStart、onProgress、onFinish、onCancel)、クライアント側の Flash、またはスクロール 管理を実装する場合は、必ずファイル全体を読んでください: references/navigation.md(約200行)— コールバックの完全な API、router.flash()、スクロール領域、履歴の暗号化について説明します。

ネストされたレイアウト、条件付きレイアウト、またはレイアウトレベルのデータ共有を実装する場合は、 必ずファイル全体を読んでください: references/layouts.md(約180行)— ネストされたレイアウトのパターン、 レイアウトプロップ、デフォルトのレイアウト設定について説明します。

基本的な <Link>、router.visit、または単一階層のレイアウトを使用する場合は、参考資料を読み込まないでください。上記の例で十分です。

コメント