Help us improve
Share bugs, ideas, or general feedback.
From majestic-rails
Builds SPAs with Inertia.js and Rails using React, Vue, or Svelte. Handles Inertia pages, useForm, shared props, flash messages, and client-side routing.
npx claudepluginhub majesticlabs-dev/majestic-marketplace --plugin majestic-railsHow this skill is triggered — by the user, by Claude, or both
Slash command
/majestic-rails:inertia-coderThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Build modern single-page applications using Inertia.js with React/Vue/Svelte and Rails backend.
Guides server-driven architecture patterns for Inertia Rails + React, with decision matrix for data loading, forms, navigation, state management. Use when building pages, CRUD, or displaying data.
Implements Hotwire Turbo (Drive, Frames, Streams, Morph) and Stimulus controllers in Rails views for SPA-like interactivity, real-time updates, and progressive enhancement.
Bootstraps a new Rails project with PostgreSQL, Inertia.js, React, Vite, Tailwind, Sidekiq, and Redis. Use when starting a modern Rails app with SPA frontend.
Share bugs, ideas, or general feedback.
Build 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