inertia-rails-pages
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInertia Rails Pages
Inertia Rails 页面开发
Page components, layouts, navigation, and client-side APIs.
Before building a page, ask:
- Does this page need a layout? → Use persistent layout (React: ; Vue:
Page.layout = ...; Svelte: module script export) — wrapping in JSX/template remounts on every navigation, losing scroll position, audio playback, and component statedefineOptions({ layout }) - Does UI state come from the URL? → Change BOTH controller (read , pass as prop) AND component (derive from prop, no
params/useState) — useuseEffectto update URLrouter.get - Need to refresh data without navigation? → — never
router.reload({ only: [...] })+useEffectfetch - Need to update a prop without server round-trip? → — no fetch, no reload
router.replaceProp
NEVER:
- Parse or use
window.location.search— derive URL state from controller propsuseSearchParams - Use /
useStateto sync URL ↔ React state — the controller passes URL-derived data as props; the component just reads themuseEffect - Pass arguments to render function —
<Deferred>does NOT work; child reads via{(data) => ...}usePage() - Access — flash is top-level:
usePage().props.flashusePage().flash - Wrap layout in JSX return for persistence — use or global layout inside createInertiaApp's resolve callback
Page.layout = ...
页面组件、布局、导航及客户端API使用指南。
在构建页面之前,请先思考:
- 该页面是否需要布局? → 使用持久化布局(React:;Vue:
Page.layout = ...;Svelte:模块脚本导出)—— 若在JSX/模板中直接包裹布局,每次导航都会重新挂载组件,导致滚动位置、音频播放状态及组件状态丢失defineOptions({ layout }) - UI状态是否来自URL? → 同时修改控制器(读取并作为props传递)和组件(从props派生状态,不使用
params/useState)—— 使用useEffect更新URLrouter.get - 无需导航即可刷新数据? → 使用—— 绝不要用
router.reload({ only: [...] })+useEffectfetch - 无需服务器往返即可更新props? → 使用—— 无需请求,无需重载
router.replaceProp
绝对禁止:
- 解析或使用
window.location.search—— 从控制器传递的props中派生URL状态useSearchParams - 使用/
useState同步URL ↔ React状态 —— 控制器会将URL派生的数据作为props传递;组件只需直接读取即可useEffect - 为的渲染函数传递参数 ——
<Deferred>无法生效;子组件需通过{(data) => ...}读取数据usePage() - 访问—— flash为顶层属性:
usePage().props.flashusePage().flash - 为了持久化将布局包裹在JSX返回值中 —— 使用或在createInertiaApp的resolve回调中配置全局布局
Page.layout = ...
Page Component Structure
页面组件结构
Pages are default exports receiving controller props as function arguments.
Use (not — causes TS2344 in React). Vue uses , Svelte uses .
type Props = { ... }interfacedefineProps<T>()let { ... } = $props()tsx
type Props = {
posts: Post[]
}
export default function Index({ posts }: Props) {
return <PostList posts={posts} />
}页面为默认导出的函数,接收控制器传递的props作为参数。
使用(不要用——会在React中引发TS2344错误)。Vue使用,Svelte使用。
type Props = { ... }interfacedefineProps<T>()let { ... } = $props()tsx
type Props = {
posts: Post[]
}
export default function Index({ posts }: Props) {
return <PostList posts={posts} />
}Persistent Layouts
持久化布局
Layouts persist across navigations — no remounting, preserving scroll, audio, etc.
tsx
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:
tsx
// 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
}布局会在导航过程中保持持久化——不会重新挂载,可保留滚动位置、音频状态等。
tsx
import { AppLayout } from '@/layouts/app-layout'
export default function Show({ course }: Props) {
return <CourseContent course={course} />
}
// 单个布局
Show.layout = (page: React.ReactNode) => <AppLayout>{page}</AppLayout>入口文件中的默认布局:
tsx
// 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
}Navigation
导航
<Link>
and router
<Link>router<Link>
和 router
<Link>routerUse for internal navigation (not ) and
for programmatic navigation. Key non-obvious features:
<Link href="..."><a>router.get/post/patch/deletetsx
// 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 API, visit options, and event callbacks are in
— see loading trigger below.
routerreferences/navigation.md内部导航使用(不要用),编程式导航使用。
以下是一些容易被忽略的关键特性:
<Link href="..."><a>router.get/post/patch/deletetsx
// 预加载 —— 鼠标悬停时预加载页面数据
<Link href="/users" prefetch>Users</Link>
<Link href="/users" prefetch cacheFor="30s">Users</Link>
// 带缓存标签的预加载 —— 数据变更后可失效缓存
<Link href="/users" prefetch cacheTags="users">Users</Link>
// 编程式预加载(例如,针对可能的下一个跳转目标)
router.prefetch('/settings', {}, { cacheFor: '1m' })
// 部分重载 —— 无需导航即可刷新指定props
router.reload({ only: ['users'] })完整的 API、访问选项及事件回调请参考——下方为加载触发相关内容。
routerreferences/navigation.mdClient-Side Prop Helpers
客户端Props辅助工具
Update props without a server round-trip:
tsx
// 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 with and
automatically set to .
router.replace()preserveScrollpreserveStatetruerouter.replaceProprouter.reloadrouter.replaceProprouter.reload无需服务器往返即可更新props:
tsx
// 替换单个props(支持点符号)
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: 'New' })
router.prependToProp('notifications', (current, props) => ({
id: Date.now(),
message: `Hello ${props.auth.user.name}`,
}))这些是的快捷方式,会自动将和设为。
router.replace()preserveScrollpreserveStatetruerouter.replaceProprouter.reloadrouter.replaceProprouter.reloadURL-Driven State (Dialogs, Tabs, Filters)
URL驱动状态(对话框、标签页、筛选器)
URL state = server state = props. ALWAYS implement both sides:
- Controller — read and pass as a prop
params - Component — derive UI state from that prop (no , no
useState)useEffect - Update — with query params to change URL (triggers server round-trip, new props arrive)
router.get
NEVER use + to sync URL ↔ dialog/tab/filter state.
The server is the single source of truth — the component just reads props.
useStateuseEffectruby
undefinedURL状态 = 服务器状态 = props。必须同时实现两端逻辑:
- 控制器 —— 读取并作为props传递
params - 组件 —— 从props派生UI状态(不使用,不使用
useState)useEffect - 更新 —— 使用携带查询参数修改URL(触发服务器往返,获取新的props)
router.get
绝对禁止使用 + 同步URL ↔ 对话框/标签页/筛选器状态。
服务器为单一数据源——组件只需读取props即可。
useStateuseEffectruby
undefinedStep 1: Controller reads params, passes as prop
步骤1:控制器读取params并作为props传递
def index
render inertia: {
users: User.all,
selected_user_id: params[:user_id]&.to_i
}
end
```tsx
// 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 fires, Inertia
makes a request to the server → controller runs with →
returns new props with → component re-renders with the
dialog open. The cycle is: URL → server → props → render. Parsing
client-side duplicates what the server already does.
router.get('/users', { user_id: 5 })params[:user_id] = 5selected_user_id: 5window.locationdef index
render inertia: {
users: User.all,
selected_user_id: params[:user_id]&.to_i
}
end
```tsx
// 步骤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? 当触发时,Inertia会向服务器发送请求 → 控制器使用执行 → 返回携带的新props → 组件重新渲染并打开对话框。流程为:URL → 服务器 → props → 渲染。在客户端解析会重复服务器已完成的工作。
router.get('/users', { user_id: 5 })params[:user_id] = 5selected_user_id: 5window.locationShared Props
共享Props
Shared props (auth, flash) are typed globally via InertiaConfig (see skill) — page components only type their OWN props:
inertia-rails-typescripttsx
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} />
}共享props(auth、flash)通过InertiaConfig进行全局类型定义(参考技能)——页面组件只需定义自身的props:
inertia-rails-typescripttsx
type Props = {
users: User[] // 仅页面专属props
// auth无需在此定义——通过InertiaConfig全局类型定义
}
export default function Index({ users }: Props) {
const { props, flash } = usePage()
// props.auth通过InertiaConfig全局类型定义,flash.notice通过InertiaConfig全局类型定义
return <UserList users={users} />
}Flash Access
Flash访问方式
Flash is top-level on the page object, NOT inside props — this is the #1
flash mistake. Flash config is in ; toast UI is in .
inertia-rails-controllersshadcn-inertiatsx
// BAD: usePage().props.flash ← WRONG, flash is not in props
// GOOD: usePage().flash ← flash.notice, flash.alertFlash是page对象的顶层属性,不在props内部——这是使用flash时最常见的错误。Flash配置请参考;Toast UI请参考。
inertia-rails-controllersshadcn-inertiatsx
// 错误写法:usePage().props.flash ← 错误,flash不在props中
// 正确写法:usePage().flash ← flash.notice、flash.alert<Deferred>
Component
<Deferred><Deferred>
组件
<Deferred>Renders fallback until deferred props arrive. Children can be plain
or render function. Either way, the child reads the deferred
prop from page props via — the render function receives no arguments.
ReactNode() => ReactNodeusePage()tsx
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>在延迟props加载完成前渲染 fallback 内容。子组件可以是普通或渲染函数。无论哪种方式,子组件都需通过从页面props中读取延迟加载的props——渲染函数不接收任何参数。
ReactNode() => ReactNodeusePage()tsx
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>
// 错误写法——渲染函数不接收data作为参数:
// <Deferred data="stats">{(data) => <Stats data={data} />}</Deferred><InfiniteScroll>
Component
<InfiniteScroll><InfiniteScroll>
组件
<InfiniteScroll>Automatic infinite scroll — loads next pages as user scrolls down. Pairs with
on the server (see ):
InertiaRails.scrollinertia-rails-controllerstsx
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: (prop name), (fallback), (button instead of auto),
(auto for first 3 pages, then button), (don't update URL).
dataloadingmanualmanualAfter={3}preserveUrl自动实现无限滚动——用户向下滚动时加载下一页。需与服务器端的配合使用(参考):
InertiaRails.scrollinertia-rails-controllerstsx
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参数:(props名称)、(fallback内容)、(使用按钮触发而非自动加载)、(前3页自动加载,之后使用按钮)、(不更新URL)。
dataloadingmanualmanualAfter={3}preserveUrl<WhenVisible>
Component
<WhenVisible><WhenVisible>
组件
<WhenVisible>Loads data when element enters viewport. Use for lazy sections (comments,
related items), NOT for infinite scroll (use above):
<InfiniteScroll>tsx
import { WhenVisible } from '@inertiajs/react'
<WhenVisible data="comments" fallback={<Spinner />}>
<CommentsList />
</WhenVisible>当元素进入视口时加载数据。适用于懒加载区块(评论、相关内容),不适用于无限滚动(请使用上述):
<InfiniteScroll>tsx
import { WhenVisible } from '@inertiajs/react'
<WhenVisible data="comments" fallback={<Spinner />}>
<CommentsList />
</WhenVisible>Troubleshooting
故障排查
| Symptom | Cause | Fix |
|---|---|---|
| Layout remounts on every navigation | Wrapping layout in JSX return instead of | Use persistent layout |
| Render function expects args | Render function receives NO arguments — use |
Flash is | Accessing | Flash is top-level: |
| URL state lost on navigation | Parsing | Derive from props — controller reads |
| Element not in viewport or prop name wrong | |
Component state resets on | Missing | Add |
| Scroll jumps to top after form submit | Missing | Add |
| 症状 | 原因 | 修复方案 |
|---|---|---|
| 每次导航时布局都会重新挂载 | 将布局包裹在JSX返回值中而非使用 | 使用持久化布局 |
| 渲染函数期望接收参数 | 渲染函数不接收任何参数——使用 |
Flash为 | 访问 | Flash为顶层属性: |
| 导航时URL状态丢失 | 在useEffect中解析 | 从props派生状态——控制器读取 |
| 元素未进入视口或props名称错误 | |
| 缺少 | 在筛选/排序/标签页切换的访问选项中添加 |
| 表单提交后滚动位置跳至顶部 | 缺少 | 在访问或表单选项中添加 |
Related Skills
相关技能
- Flash config → (flash_keys)
inertia-rails-controllers - Flash toast UI → (Sonner + useFlash)
shadcn-inertia - Shared props typing → (InertiaConfig)
inertia-rails-typescript - Deferred server-side → (InertiaRails.defer)
inertia-rails-controllers - URL-driven dialogs → (Dialog component)
shadcn-inertia
- Flash配置 → (flash_keys)
inertia-rails-controllers - Flash提示UI → (Sonner + useFlash)
shadcn-inertia - 共享Props类型定义 → (InertiaConfig)
inertia-rails-typescript - 服务端延迟加载 → (InertiaRails.defer)
inertia-rails-controllers - URL驱动对话框 → (Dialog组件)
shadcn-inertia
Vue / Svelte
Vue / Svelte
All examples above use React syntax. For Vue 3 or Svelte equivalents:
- Vue 3: —
references/vue.md,definePropscomposable, scoped slots forusePage()/<Deferred>/<WhenVisible>,<InfiniteScroll>for persistent layoutsdefineOptions({ layout }) - Svelte: —
references/svelte.md,$props()store,$pagesyntax for{#snippet}/<Deferred>/<WhenVisible>,<InfiniteScroll>instead of<svelte:head>, module script layout export<Head>
MANDATORY — 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.
上述示例均使用React语法。如需Vue 3或Svelte的等效实现:
- Vue 3:——
references/vue.md、defineProps组合式API、usePage()/<Deferred>/<WhenVisible>的作用域插槽、<InfiniteScroll>定义持久化布局defineOptions({ layout }) - Svelte:——
references/svelte.md、$props()存储、$page/<Deferred>/<WhenVisible>的<InfiniteScroll>语法、使用{#snippet}替代<svelte:head>、模块脚本导出布局<Head>
强制要求——若项目使用Vue或Svelte,请阅读对应文档。上述概念和禁止规则适用于所有框架,但代码语法有所不同。
References
参考文档
MANDATORY — READ ENTIRE FILE when implementing event callbacks (,
, , , ), client-side flash, or scroll
management:
(~200 lines) — full callback
API, , scroll regions, and history encryption.
onBeforeonStartonProgressonFinishonCancelreferences/navigation.mdrouter.flash()MANDATORY — READ ENTIRE FILE when implementing nested layouts, conditional
layouts, or layout-level data sharing:
(~180 lines) — nested layout patterns,
layout props, and default layout configuration.
references/layouts.mdDo NOT load references for basic , , or single-level
layout usage — the examples above are sufficient.
<Link>router.visit强制要求——实现事件回调(、、、、)、客户端flash或滚动管理时,请完整阅读该文档:
(约200行)——完整的回调API、、滚动区域及历史记录加密。
onBeforeonStartonProgressonFinishonCancelreferences/navigation.mdrouter.flash()强制要求——实现嵌套布局、条件布局或布局级数据共享时,请完整阅读该文档:
(约180行)——嵌套布局模式、布局props及默认布局配置。
references/layouts.md基础使用场景(如、、单层布局)无需阅读参考文档——上述示例已足够覆盖。
<Link>router.visit