mcpField
I
inertia-rails-pages

inertia-rails-pages

github.com/inertia-rails开发70506 次安装37 周前更新
代码仓库

关于 inertia-rails-pages

面向使用 Inertia.js 构建页面的开发者,介绍 React 页面组件、布局和客户端 API 的实现方式。内容涵盖持久化布局、基于 URL 属性的界面状态、导航以及局部刷新。

主要功能

  1. 01页面组件与属性组织
  2. 02持久化布局及状态保留
  3. 03基于 URL 属性的界面状态
  4. 04router 导航与客户端属性更新
  5. 05延迟属性和可见区域加载

适用场景

  1. 01搭建 Inertia.js 页面及持久化布局
  2. 02用 URL 参数驱动对话框或筛选器
  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,作为 prop 传入)和组件(根据 prop 派生状态,不使用 useState/useEffect)——使用 router.get 更新 URL
  • 需要在不导航的情况下刷新数据吗? → 使用 router.reload({ only: [...] })——绝不要使用 useEffect + fetch
  • 需要在不经过服务器往返的情况下更新 prop 吗? → 使用 router.replaceProp——无需 fetch,也无需重新加载

绝不要:

  • 解析 window.location.search 或使用 useSearchParams——根据控制器 props 派生 URL 状态
  • 使用 useState/useEffect 同步 URL ↔ React 状态——控制器会将根据 URL 得出的数据作为 props 传入;组件只需读取这些数据
  • 向 <Deferred> 渲染函数传递参数——{(data) => ...} 不起作用;子组件通过 usePage() 读取数据
  • 访问 usePage().props.flash——flash 位于顶层:usePage().flash
  • 在 JSX 返回值中包裹布局以实现持久化——使用 Page.layout = ...,或在 createInertiaApp 的 resolve 回调中设置全局布局

页面组件结构

页面是接收控制器 props 作为函数参数的默认导出组件。 使用 type Props = { ... }(而不是 interface——在 React 中会导致 TS2344)。Vue 使用 defineProps<T>(),Svelte 使用 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

内部导航使用 <Link href="...">(不要使用 <a>);程序化导航使用 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——另请参阅下方的加载触发方式。

客户端 Prop 辅助方法

无需经过服务器往返即可更新 props:

// 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}`,
}))

这些是 router.replace() 的快捷方式,会自动将 preserveScroll 和 preserveState 设为 true。

router.replaceProp 与 router.reload: 对于仅限客户端的状态更改(切换模态框、递增计数器),使用 router.replaceProp——无需经过服务器往返。需要从服务器获取最新数据(更新后的记录、重新计算的统计数据)时,使用 router.reload。

URL 驱动的状态(对话框、标签页、筛选器)

URL 状态 = 服务器状态 = props。务必同时实现两端:

  1. 控制器——读取 params 并作为 prop 传入
  2. 组件——根据该 prop 派生 UI 状态(不使用 useState,不使用 useEffect)
  3. 更新——使用带查询参数的 router.get 更改 URL(触发服务器往返,并返回新的 props)

绝不要使用 useState + useEffect 同步 URL ↔ 对话框/标签页/筛选器状态。 服务器是唯一可信来源——组件只需读取 props。

# 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 的新 props → 组件重新渲染并打开 对话框。流程为:URL → 服务器 → props → 渲染。在客户端解析 window.location,只是在重复服务器已经完成的工作。

共享 Props

共享 props(auth、flash)通过 InertiaConfig 进行全局类型定义(参见 inertia-rails-typescript 技能)——页面组件只需为自己的 props 添加类型:

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;toast 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>

故障排查

症状原因修复方法
每次导航时布局都会重新挂载在 JSX 返回值中包装布局,而不是使用 Page.layout使用持久布局
Deferred 子节点始终不渲染渲染函数需要参数 {(data) => ...}渲染函数不接收任何参数——使用 {() => <Child />} 或普通的 <Child />。子节点通过 usePage() 读取属性
Flash 为 undefined访问 usePage().props.flashFlash 位于顶层:使用 usePage().flash,而不是放在 props 内
导航后 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 store、用于 <Deferred>/<WhenVisible>/<InfiniteScroll> 的 {#snippet} 语法、使用 <svelte:head> 代替 <Head>、模块脚本布局导出

**必须——当项目使用 Vue 或 Svelte 时,阅读对应文件。**以上概念和 NEVER 规则适用于所有框架,但代码语法有所不同。

参考资料

必须——实现事件回调(onBefore、 onStart、onProgress、onFinish、onCancel)、客户端 Flash 或滚动管理时,阅读整个文件: references/navigation.md(约 200 行)——完整的回调 API、router.flash()、滚动区域和历史记录加密。

必须——实现嵌套布局、条件布局或布局级数据共享时,阅读整个文件: references/layouts.md(约 180 行)——嵌套布局模式、 布局属性和默认布局配置。

不要加载用于基础 <Link>、router.visit 或单层 布局用法的参考资料——以上示例已足够。

评论

支持 Markdown