重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
inertia-rails-pages by inertia-rails/skills
npx skills add https://github.com/inertia-rails/skills --skill inertia-rails-pages页面组件、布局、导航和客户端 API。
构建页面之前,请思考:
Page.layout = ...;Vue:defineOptions({ layout });Svelte:模块脚本导出)——在 JSX/模板中包装会在每次导航时重新挂载,导致滚动位置、音频播放和组件状态丢失params,作为 prop 传递)和组件(从 prop 派生,不使用 useState/useEffect)——使用 router.get 更新 URLrouter.reload({ only: [...] })——切勿使用 useEffect + 广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
fetchrouter.replaceProp——无需 fetch,无需 reload切勿:
window.location.search 或使用 useSearchParams——从控制器传递的 props 中派生 URL 状态useState/useEffect 来同步 URL ↔ React 状态——控制器将 URL 派生的数据作为 props 传递;组件只需读取它们<Deferred> 渲染函数传递参数——{(data) => ...} 无效;子组件通过 usePage() 读取数据usePage().props.flash——flash 是顶层的:usePage().flashPage.layout = ... 或全局布局页面是默认导出,接收控制器传递的 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} />
}
// 单一布局
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> // 如果未设置,则使用默认布局
return page
}
<Link> 和 router使用 <Link href="..."> 进行内部导航(不要用 <a>),使用 router.get/post/patch/delete 进行编程式导航。关键的非明显特性:
// 预取——悬停时预加载页面数据
<Link href="/users" prefetch>用户</Link>
<Link href="/users" prefetch cacheFor="30s">用户</Link>
// 带缓存标签的预取——在变更后失效
<Link href="/users" prefetch cacheTags="users">用户</Link>
// 编程式预取(例如,可能的下一个目的地)
router.prefetch('/settings', {}, { cacheFor: '1m' })
// 部分重载——在不导航的情况下刷新特定 props
router.reload({ only: ['users'] })
完整的 router API、访问选项和事件回调在 references/navigation.md 中——请参见下面的加载触发器。
无需服务器往返即可更新 props:
// 替换单个 prop(支持点号表示法)
router.replaceProp('show_modal', false)
router.replaceProp('user.name', 'Jane Smith')
// 使用回调(接收当前值和所有 props)
router.replaceProp('count', (current) => current + 1)
// 追加/前置到数组 props
router.appendToProp('messages', { id: 4, text: '新消息' })
router.prependToProp('notifications', (current, props) => ({
id: Date.now(),
message: `你好 ${props.auth.user.name}`,
}))
这些是 router.replace() 的快捷方式,会自动将 preserveScroll 和 preserveState 设置为 true。
router.replaceProp 与 router.reload 的区别: 使用 router.replaceProp 处理仅限客户端的状态变更(切换模态框、增加计数器)——无需服务器往返。使用 router.reload 当你需要从服务器获取新数据时(更新的记录、重新计算的统计信息)。
URL 状态 = 服务器状态 = props。务必同时实现两端:
params 并作为 prop 传递useState,不使用 useEffect)router.get 并附带查询参数来更改 URL(触发服务器往返,新的 props 到达)切勿使用 useState + useEffect 来同步 URL ↔ 对话框/标签页/筛选器状态。服务器是单一事实来源——组件只需读取 props。
# 步骤 1:控制器读取 params,作为 prop 传递
def index
render inertia: {
users: User.all,
selected_user_id: params[:user_id]&.to_i
}
end
// 步骤 2+3:从 props 派生状态,使用 router.get 更新 URL
type Props = {
users: User[]
selected_user_id: number | null // 来自控制器
}
export default function Index({ users, selected_user_id }: Props) {
// 派生——不使用 useState,不使用 useEffect,不解析 window.location
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(auth、flash)通过 InertiaConfig 全局类型化(参见 inertia-rails-typescript 技能)——页面组件仅类型化其自身的 props:
type Props = {
users: User[] // 仅页面特定
// auth 不在这里——通过 InertiaConfig 全局类型化
}
export default function Index({ users }: Props) {
const { props, flash } = usePage()
// props.auth 通过 InertiaConfig 类型化,flash.notice 通过 InertiaConfig 类型化
return <UserList users={users} />
}
Flash 位于页面对象的顶层,不在 props 内部——这是 flash 最常见的错误。Flash 配置在 inertia-rails-controllers 中;toast UI 在 shadcn-inertia 中。
// 错误: usePage().props.flash ← 错误,flash 不在 props 中
// 正确: usePage().flash ← flash.notice, flash.alert
<Deferred> 组件在延迟的 props 到达之前渲染回退内容。子元素可以是普通的 ReactNode 或 () => ReactNode 渲染函数。无论哪种方式,子组件都通过 usePage() 从页面 props 中读取延迟的 prop——渲染函数不接收任何参数。
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>
</>
)
}
// 同样有效——渲染函数(无参数,子组件仍通过 usePage 读取):
// <Deferred data="stats" fallback={<Spinner />}>
// {() => <Stats />}
// </Deferred>
// 错误——渲染函数不接收数据作为参数:
// <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>
)
}
Props:data(prop 名称)、loading(回退内容)、manual(按钮代替自动加载)、manualAfter={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() 读取 prop |
Flash 是 undefined | 访问 usePage().props.flash | Flash 是顶层的:usePage().flash,不在 props 内部 |
| 导航时 URL 状态丢失 | 在 useEffect 中解析 window.location | 从 props 派生——控制器读取 params 并作为 prop 传递 |
WhenVisible 从不触发 | 元素不在视口中或 prop 名称错误 | data 必须与控制器在部分重载时提供的 prop 名称匹配 |
router.get 时组件状态重置 | 缺少 preserveState: true | 为筛选/排序/标签页变更添加 preserveState: true 到访问选项 |
| 表单提交后滚动跳转到顶部 | 缺少 preserveScroll | 在访问或表单选项中添加 preserveScroll: true |
inertia-rails-controllers (flash_keys)shadcn-inertia (Sonner + useFlash)inertia-rails-typescript (InertiaConfig)inertia-rails-controllers (InertiaRails.defer)shadcn-inertia (Dialog 组件)以上所有示例均使用 React 语法。Vue 3 或 Svelte 的等效用法:
references/vue.md —— defineProps、usePage() 组合式函数、<Deferred>/<WhenVisible>/<InfiniteScroll> 的作用域插槽、defineOptions({ layout }) 用于持久化布局references/svelte.md —— $props()、$page 存储、<Deferred>/<WhenVisible>/<InfiniteScroll> 的 {#snippet} 语法、<svelte:head> 代替 <Head>、模块脚本布局导出强制要求——当项目使用 Vue 或 Svelte 时,请阅读对应的文件。 上述概念和“切勿”规则适用于所有框架,但代码语法不同。
强制要求——当实现事件回调(onBefore、onStart、onProgress、onFinish、onCancel)、客户端 flash 或滚动管理时,请阅读整个文件: references/navigation.md (~200 行) ——完整的回调 API、router.flash()、滚动区域和历史记录加密。
强制要求——当实现嵌套布局、条件布局或布局级数据共享时,请阅读整个文件: references/layouts.md (~180 行) ——嵌套布局模式、布局 props 和默认布局配置。
不要加载用于基本 <Link>、router.visit 或单层布局使用的参考资料——以上示例已足够。
每周安装次数
67
仓库
GitHub 星标数
35
首次出现
2026年2月13日
安全审计
安装于
gemini-cli66
codex66
opencode66
github-copilot65
amp64
kimi-cli63
Page components, layouts, navigation, and client-side APIs.
Before building a page, ask:
Page.layout = ...; Vue: defineOptions({ layout }); Svelte: module script export) — wrapping in JSX/template remounts on every navigation, losing scroll position, audio playback, and component stateparams, pass as prop) AND component (derive from prop, no useState/useEffect) — use router.get to update URLrouter.reload({ only: [...] }) — never useEffect + fetchrouter.replaceProp — no fetch, no reloadNEVER:
window.location.search or use useSearchParams — derive URL state from controller propsuseState/useEffect to sync URL ↔ React state — the controller passes URL-derived data as props; the component just reads them<Deferred> render function — {(data) => ...} does NOT work; child reads via usePage()usePage().props.flash — flash is top-level: usePage().flashPage.layout = ... or global layout inside createInertiaApp's resolve callbackPages are default exports receiving controller props as function arguments. Use type Props = { ... } (not interface — causes TS2344 in React). Vue uses defineProps<T>(), Svelte uses let { ... } = $props().
type Props = {
posts: Post[]
}
export default function Index({ posts }: Props) {
return <PostList posts={posts} />
}
Layouts persist across navigations — no remounting, preserving scroll, audio, etc.
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>
Default layout in entrypoint:
// 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> and routerUse <Link href="..."> for internal navigation (not <a>) and router.get/post/patch/delete for programmatic navigation. Key non-obvious features:
// 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'] })
Full router API, visit options, and event callbacks are in references/navigation.md — see loading trigger below.
Update props without a server round-trip:
// 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}`,
}))
These are shortcuts to router.replace() with preserveScroll and preserveState automatically set to true.
router.replaceProp vs router.reload: Use router.replaceProp for client-only state changes (toggling a modal, incrementing a counter) — no server round-trip. Use router.reload when you need fresh data from the server (updated records, recalculated stats).
URL state = server state = props. ALWAYS implement both sides:
params and pass as a propuseState, no useEffect)router.get with query params to change URL (triggers server round-trip, new props arrive)NEVER use useState + useEffect to sync URL ↔ dialog/tab/filter state. The server is the single source of truth — the component just reads 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>
)
}
Why not useEffect? When router.get('/users', { user_id: 5 }) fires, Inertia makes a request to the server → controller runs with params[:user_id] = 5 → returns new props with selected_user_id: 5 → component re-renders with the dialog open. The cycle is: URL → server → props → render. Parsing window.location client-side duplicates what the server already does.
Shared props (auth, flash) are typed globally via InertiaConfig (see inertia-rails-typescript skill) — page components only type their OWN 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 is top-level on the page object, NOT inside props — this is the #1 flash mistake. Flash config is in inertia-rails-controllers; toast UI is in shadcn-inertia.
// BAD: usePage().props.flash ← WRONG, flash is not in props
// GOOD: usePage().flash ← flash.notice, flash.alert
<Deferred> ComponentRenders fallback until deferred props arrive. Children can be plain ReactNode or () => ReactNode render function. Either way, the child reads the deferred prop from page props via usePage() — the render function receives no arguments.
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> ComponentAutomatic infinite scroll — loads next pages as user scrolls down. Pairs with InertiaRails.scroll on the server (see 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>
)
}
Props: data (prop name), loading (fallback), manual (button instead of auto), manualAfter={3} (auto for first 3 pages, then button), preserveUrl (don't update URL).
<WhenVisible> ComponentLoads data when element enters viewport. Use for lazy sections (comments, related items), NOT for infinite scroll (use <InfiniteScroll> above):
import { WhenVisible } from '@inertiajs/react'
<WhenVisible data="comments" fallback={<Spinner />}>
<CommentsList />
</WhenVisible>
| Symptom | Cause | Fix |
|---|---|---|
| Layout remounts on every navigation | Wrapping layout in JSX return instead of Page.layout | Use persistent layout |
Deferred children never render | Render function expects args {(data) => ...} | Render function receives NO arguments — use {() => <Child />} or plain <Child />. Child reads prop via usePage() |
Flash is undefined |
inertia-rails-controllers (flash_keys)shadcn-inertia (Sonner + useFlash)inertia-rails-typescript (InertiaConfig)inertia-rails-controllers (InertiaRails.defer)shadcn-inertia (Dialog component)All examples above use React syntax. For Vue 3 or Svelte equivalents:
references/vue.md — defineProps, usePage() composable, scoped slots for <Deferred>/<WhenVisible>/<InfiniteScroll>, defineOptions({ layout }) for persistent layoutsreferences/svelte.md — $props(), store, syntax for //, instead of , module script layout exportMANDATORY — READ THE MATCHING FILE when the project uses Vue or Svelte. The concepts and NEVER rules above apply to all frameworks, but code syntax differs.
MANDATORY — READ ENTIRE FILE when implementing event callbacks (onBefore, onStart, onProgress, onFinish, onCancel), client-side flash, or scroll management: references/navigation.md (~200 lines) — full callback API, router.flash(), scroll regions, and history encryption.
MANDATORY — READ ENTIRE FILE when implementing nested layouts, conditional layouts, or layout-level data sharing: references/layouts.md (~180 lines) — nested layout patterns, layout props, and default layout configuration.
Do NOT load references for basic <Link>, router.visit, or single-level layout usage — the examples above are sufficient.
Weekly Installs
67
Repository
GitHub Stars
35
First Seen
Feb 13, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli66
codex66
opencode66
github-copilot65
amp64
kimi-cli63
GSAP React 动画库使用指南:useGSAP Hook 与最佳实践
3,400 周安装
Visual Planner:基于tldraw的可视化工作流与智能体编排图表工具
6 周安装
create-mcp-app 快速构建交互式 MCP 应用 - 工具与资源整合指南
7 周安装
Nuxt Studio 设置与部署指南:为Nuxt Content网站添加可视化CMS编辑器
13 周安装
自主开发代理交付工单技能:AI自动化代码任务处理与PR交付
8 周安装
JSON 转 React Email 渲染器:用 JSON 规范生成 HTML/纯文本邮件 | @json-render/react-email
8 周安装
runtime-context技能:AI智能体运行时环境检测与工具适配,实现跨平台兼容性
9 周安装
Accessing usePage().props.flash |
Flash is top-level: usePage().flash, not inside props |
| URL state lost on navigation | Parsing window.location in useEffect | Derive from props — controller reads params and passes as prop |
WhenVisible never triggers | Element not in viewport or prop name wrong | data must match a prop name the controller provides on partial reload |
Component state resets on router.get | Missing preserveState: true | Add preserveState: true to visit options for filter/sort/tab changes |
| Scroll jumps to top after form submit | Missing preserveScroll | Add preserveScroll: true to the visit or form options |
$page{#snippet}<Deferred><WhenVisible><InfiniteScroll><svelte:head><Head>