Build modern SPAs with Inertia.js and Rails using React, Vue, or Svelte. Use when creating Inertia pages, handling forms with useForm, managing shared props, or implementing client-side routing. Triggers on Inertia.js setup, SPA development, or React/Vue/Svelte with Rails.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/react-patterns.mdreferences/svelte-patterns.mdreferences/vue-patterns.mdBuild modern single-page applications using Inertia.js with React/Vue/Svelte and Rails backend.
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 |
# Gemfile
gem 'inertia_rails'
gem 'vite_rails'
bundle install
rails inertia:install # Choose: React, Vue, or Svelte
# 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
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
end
// 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>
)
}
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>
)
}
// 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>
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>
<Link> instead of <a> tagsprocessingwindow.location - breaks SPAFor framework-specific patterns:
references/react-patterns.md - React hooks, TypeScript, advanced patternsreferences/vue-patterns.md - Vue 3 Composition API patternsreferences/svelte-patterns.md - Svelte stores and reactivity patterns