关于 inertia-rails-pages
面向使用 Inertia.js 构建页面的开发者,介绍 React 页面组件、布局和客户端 API 的实现方式。内容涵盖持久化布局、基于 URL 属性的界面状态、导航以及局部刷新。
主要功能
- 01页面组件与属性组织
- 02持久化布局及状态保留
- 03基于 URL 属性的界面状态
- 04router 导航与客户端属性更新
- 05延迟属性和可见区域加载
适用场景
- 01搭建 Inertia.js 页面及持久化布局
- 02用 URL 参数驱动对话框或筛选器
- 03无需导航刷新部分页面数据
- 04按需加载延迟数据或可见区域内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ inertia-rails/ skills --skill inertia-rails-pages安装后在 Claude Code 中输入/inertia-rails-pages确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/layouts.mdnavigation.mdsvelte.mdvue.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。务必同时实现两端:
- 控制器——读取
params并作为 prop 传入 - 组件——根据该 prop 派生 UI 状态(不使用
useState,不使用useEffect) - 更新——使用带查询参数的
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.flash | Flash 位于顶层:使用 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()、$pagestore、用于<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 或单层
布局用法的参考资料——以上示例已足够。