inertia-coder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseInertia.js + Rails
Inertia.js + Rails
Build modern single-page applications using Inertia.js with React/Vue/Svelte and Rails backend.
使用Inertia.js搭配React/Vue/Svelte前端与Rails后端,构建现代化单页应用。
When to Use This Skill
何时使用此技能
- Setting up Inertia.js with Rails
- Creating Inertia page components
- Handling forms with useForm hook
- Managing shared props and flash messages
- Client-side routing without API complexity
- File uploads with progress tracking
- 搭建Inertia.js与Rails的集成环境
- 创建Inertia页面组件
- 使用useForm钩子处理表单
- 管理共享props与flash消息
- 无需复杂API即可实现客户端路由
- 带进度追踪的文件上传
What is Inertia.js?
什么是Inertia.js?
Inertia.js allows you to build SPAs using classic server-side routing and controllers.
| Approach | Pros | Cons |
|---|---|---|
| Traditional Rails Views | Simple, server-rendered | Limited interactivity |
| Rails API + React SPA | Full SPA experience | Duplicated routing, complex state |
| Inertia.js | SPA + server routing | Best of both worlds |
Inertia.js允许你使用传统的服务端路由与控制器来构建SPA。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 传统Rails视图 | 简单,服务端渲染 | 交互性有限 |
| Rails API + React SPA | 完整的SPA体验 | 路由重复,状态管理复杂 |
| Inertia.js | SPA + 服务端路由 | 兼顾两者优势 |
Quick Setup
快速搭建
ruby
undefinedruby
undefinedGemfile
Gemfile
gem 'inertia_rails'
gem 'vite_rails'
```bash
bundle install
rails inertia:install # Choose: React, Vue, or Svelteruby
undefinedgem 'inertia_rails'
gem 'vite_rails'
```bash
bundle install
rails inertia:install # Choose: React, Vue, or Svelteruby
undefinedconfig/initializers/inertia_rails.rb
config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.version = ViteRuby.digest
config.share do |controller|
{
auth: {
user: controller.current_user&.as_json(only: [:id, :name, :email])
},
flash: controller.flash.to_hash
}
end
end
undefinedInertiaRails.configure do |config|
config.version = ViteRuby.digest
config.share do |controller|
{
auth: {
user: controller.current_user&.as_json(only: [:id, :name, :email])
},
flash: controller.flash.to_hash
}
end
end
undefinedController Pattern
控制器模式
ruby
class ArticlesController < ApplicationController
def index
articles = Article.published.order(created_at: :desc)
render inertia: 'Articles/Index', props: {
articles: articles.as_json(only: [:id, :title, :excerpt])
}
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to article_path(@article), notice: 'Article created'
else
redirect_to new_article_path, inertia: { errors: @article.errors }
end
end
endruby
class ArticlesController < ApplicationController
def index
articles = Article.published.order(created_at: :desc)
render inertia: 'Articles/Index', props: {
articles: articles.as_json(only: [:id, :title, :excerpt])
}
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to article_path(@article), notice: 'Article created'
else
redirect_to new_article_path, inertia: { errors: @article.errors }
end
end
endReact Page Component
React页面组件
jsx
// app/frontend/pages/Articles/Index.jsx
import { Link } from '@inertiajs/react'
export default function Index({ articles }) {
return (
<div>
<h1>Articles</h1>
{articles.map(article => (
<Link key={article.id} href={`/articles/${article.id}`}>
<h2>{article.title}</h2>
</Link>
))}
</div>
)
}jsx
// app/frontend/pages/Articles/Index.jsx
import { Link } from '@inertiajs/react'
export default function Index({ articles }) {
return (
<div>
<h1>Articles</h1>
{articles.map(article => (
<Link key={article.id} href={`/articles/${article.id}`}>
<h2>{article.title}</h2>
</Link>
))}
</div>
)
}Forms with useForm
使用useForm处理表单
jsx
import { useForm } from '@inertiajs/react'
export default function New() {
const { data, setData, post, processing, errors } = useForm({
title: '',
body: ''
})
function handleSubmit(e) {
e.preventDefault()
post('/articles')
}
return (
<form onSubmit={handleSubmit}>
<input
value={data.title}
onChange={e => setData('title', e.target.value)}
/>
{errors.title && <div className="error">{errors.title}</div>}
<textarea
value={data.body}
onChange={e => setData('body', e.target.value)}
/>
{errors.body && <div className="error">{errors.body}</div>}
<button disabled={processing}>
{processing ? 'Creating...' : 'Create'}
</button>
</form>
)
}jsx
import { useForm } from '@inertiajs/react'
export default function New() {
const { data, setData, post, processing, errors } = useForm({
title: '',
body: ''
})
function handleSubmit(e) {
e.preventDefault()
post('/articles')
}
return (
<form onSubmit={handleSubmit}>
<input
value={data.title}
onChange={e => setData('title', e.target.value)}
/>
{errors.title && <div className="error">{errors.title}</div>}
<textarea
value={data.body}
onChange={e => setData('body', e.target.value)}
/>
{errors.body && <div className="error">{errors.body}</div>}
<button disabled={processing}>
{processing ? 'Creating...' : 'Create'}
</button>
</form>
)
}Shared Layout
共享布局
jsx
// app/frontend/layouts/AppLayout.jsx
import { Link, usePage } from '@inertiajs/react'
export default function AppLayout({ children }) {
const { auth, flash } = usePage().props
return (
<div>
<nav>
<Link href="/">Home</Link>
{auth.user ? (
<Link href="/logout" method="delete">Logout</Link>
) : (
<Link href="/login">Login</Link>
)}
</nav>
{flash.success && <div className="alert-success">{flash.success}</div>}
<main>{children}</main>
</div>
)
}
// Assign layout to page
Index.layout = page => <AppLayout>{page}</AppLayout>jsx
// app/frontend/layouts/AppLayout.jsx
import { Link, usePage } from '@inertiajs/react'
export default function AppLayout({ children }) {
const { auth, flash } = usePage().props
return (
<div>
<nav>
<Link href="/">Home</Link>
{auth.user ? (
<Link href="/logout" method="delete">Logout</Link>
) : (
<Link href="/login">Login</Link>
)}
</nav>
{flash.success && <div className="alert-success">{flash.success}</div>}
<main>{children}</main>
</div>
)
}
// Assign layout to page
Index.layout = page => <AppLayout>{page}</AppLayout>File Upload
文件上传
jsx
import { useForm } from '@inertiajs/react'
const { data, setData, post, progress } = useForm({
avatar: null
})
<input
type="file"
onChange={e => setData('avatar', e.target.files[0])}
/>
{progress && <progress value={progress.percentage} max="100" />}
<button onClick={() => post('/profile/avatar', { forceFormData: true })}>
Upload
</button>jsx
import { useForm } from '@inertiajs/react'
const { data, setData, post, progress } = useForm({
avatar: null
})
<input
type="file"
onChange={e => setData('avatar', e.target.files[0])}
/>
{progress && <progress value={progress.percentage} max="100" />}
<button onClick={() => post('/profile/avatar', { forceFormData: true })}>
Upload
</button>Best Practices
最佳实践
DO
建议
- Use instead of
<Link>tags<a> - Share common data via config (auth, flash)
- Validate on server - client is UX only
- Show loading states with
processing - Use layouts for consistent navigation
- 使用标签而非
<Link>标签<a> - 通过配置共享通用数据(如认证信息、flash消息)
- 在服务端进行验证——客户端验证仅用于提升用户体验
- 使用状态展示加载中效果
processing - 使用布局保持导航一致性
DON'T
避免
- Don't use - breaks SPA
window.location - Don't create REST APIs - Inertia doesn't need them
- Don't fetch data client-side - server provides props
- Don't bypass Inertia router - breaks behavior
- 不要使用——会破坏SPA特性
window.location - 不要创建REST API——Inertia不需要它们
- 不要在客户端获取数据——由服务端提供props
- 不要绕过Inertia路由——会破坏原有行为
Detailed References
详细参考资料
For framework-specific patterns:
- - React hooks, TypeScript, advanced patterns
references/react-patterns.md - - Vue 3 Composition API patterns
references/vue-patterns.md - - Svelte stores and reactivity patterns
references/svelte-patterns.md
针对各框架的特定模式:
- - React钩子、TypeScript、进阶模式
references/react-patterns.md - - Vue 3组合式API模式
references/vue-patterns.md - - Svelte存储与响应式模式
references/svelte-patterns.md