Claude Code Plugins

Community-maintained marketplace

Feedback

Specialized skill for building modern SPAs with Inertia.js, React/Vue/Svelte, and Rails. Use when creating Inertia pages, handling forms, implementing client-side routing, managing shared data, or building interactive frontends. Alternative to traditional Rails views or full API approach.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rails-inertia
description Specialized skill for building modern SPAs with Inertia.js, React/Vue/Svelte, and Rails. Use when creating Inertia pages, handling forms, implementing client-side routing, managing shared data, or building interactive frontends. Alternative to traditional Rails views or full API approach.

Rails + Inertia.js Specialist

Build modern single-page applications using Inertia.js with React/Vue/Svelte and Rails backend.

When to Use This Skill

  • Setting up Inertia.js with Rails
  • Creating Inertia pages (React, Vue, or Svelte components)
  • Handling forms with Inertia
  • Client-side routing without full SPA complexity
  • Managing shared props and flash messages
  • File uploads and form submissions
  • Authentication and authorization with Inertia

What is Inertia.js?

Inertia.js allows you to build SPAs using classic server-side routing and controllers.

Why Inertia?

Traditional Rails Views:

  • ❌ Limited interactivity
  • ❌ Full page reloads

Rails API + React SPA:

  • ❌ Duplicated routing (backend + frontend)
  • ❌ Complex state management

Inertia.js (Best of both):

  • ✅ SPA experience with no page reloads
  • ✅ Server-side routing (Rails routes only)
  • ✅ No API layer needed
  • ✅ Use React/Vue/Svelte for views
  • ✅ Simple mental model

Quick Setup

# 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

Basic Controller Pattern

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
    outcome = Articles::Create.run(article_params)

    if outcome.valid?
      redirect_to article_path(outcome.result),
        notice: 'Article created'
    else
      redirect_to new_article_path,
        inertia: { errors: outcome.errors }
    end
  end
end

Page Components

React

// 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>
  )
}

Vue

<!-- app/frontend/pages/Articles/Index.vue -->
<script setup>
import { Link } from '@inertiajs/vue3'

defineProps({
  articles: Array
})
</script>

<template>
  <div>
    <h1>Articles</h1>
    <Link
      v-for="article in articles"
      :key="article.id"
      :href="`/articles/${article.id}`"
    >
      <h2>{{ article.title }}</h2>
    </Link>
  </div>
</template>

Svelte

<!-- app/frontend/pages/Articles/Index.svelte -->
<script>
  import { Link } from '@inertiajs/svelte'
  export let articles = []
</script>

<div>
  <h1>Articles</h1>
  {#each articles as article (article.id)}
    <Link href="/articles/{article.id}">
      <h2>{article.title}</h2>
    </Link>
  {/each}
</div>

Forms with useForm

React

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>{errors.title}</div>}

      <textarea
        value={data.body}
        onChange={e => setData('body', e.target.value)}
      />
      {errors.body && <div>{errors.body}</div>}

      <button disabled={processing}>
        {processing ? 'Creating...' : 'Create'}
      </button>
    </form>
  )
}

Svelte

<script>
  import { useForm } from '@inertiajs/svelte'

  let form = useForm({
    title: '',
    body: ''
  })
</script>

<form on:submit|preventDefault={() => $form.post('/articles')}>
  <input bind:value={$form.title} />
  {#if $form.errors.title}
    <div>{$form.errors.title}</div>
  {/if}

  <textarea bind:value={$form.body} />
  {#if $form.errors.body}
    <div>{$form.errors.body}</div>
  {/if}

  <button disabled={$form.processing}>
    {$form.processing ? 'Creating...' : 'Create'}
  </button>
</form>

Key Patterns

Shared Layout

// 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>{flash.success}</div>}
      {flash.error && <div>{flash.error}</div>}

      <main>{children}</main>
    </div>
  )
}

// Use in page:
Index.layout = page => <AppLayout>{page}</AppLayout>

File Upload

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>

Authorization

import { usePage } from '@inertiajs/react'

export default function Show({ article }) {
  const { auth } = usePage().props

  return (
    <div>
      <h1>{article.title}</h1>

      {auth.can('update', article) && (
        <Link href={`/articles/${article.id}/edit`}>Edit</Link>
      )}

      {auth.can('destroy', article) && (
        <Link href={`/articles/${article.id}`} method="delete">
          Delete
        </Link>
      )}
    </div>
  )
}

Testing

Component Test

import { render, screen } from '@testing-library/react'
import Index from '@/pages/Articles/Index'

test('renders articles', () => {
  const articles = [
    { id: 1, title: 'First Article' }
  ]

  render(<Index articles={articles} />)
  expect(screen.getByText('First Article')).toBeInTheDocument()
})

System Test

RSpec.describe "Articles", type: :system do
  it "creates article" do
    visit new_article_path

    fill_in "Title", with: "New Article"
    click_button "Create Article"

    expect(page).to have_text("Article created")
  end
end

Best Practices

✅ Do

  • Use Inertia links - <Link> instead of <a>
  • Share common data - via InertiaRails.configure
  • Validate on server - Never trust client alone
  • Handle loading states - Show spinners when processing
  • Use layouts - DRY up navigation

❌ Don't

  • Don't use window.location - Use Inertia router
  • Don't fetch data client-side - Server provides props
  • Don't create API endpoints - Use regular Rails routes
  • Don't bypass Inertia links - Breaks SPA behavior

Reference Documentation

For comprehensive examples and advanced patterns:

  • Full Inertia guide: inertia-reference.md (detailed setup, SSR, modals, pagination, all frameworks)

Remember: Inertia.js is glue between Rails and React/Vue/Svelte. Think of it as rendering React/Vue/Svelte views from Rails controllers instead of ERB templates.