Install
$ agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Inertia Rails Cookbook
Practical recipes for common patterns and integrations in Inertia Rails applications.
Working with the Official Starter Kits
The official starter kits provide a complete foundation. Here's how to customize them for your needs.
Starter Kit Structure (React)
app/
├── controllers/
│ ├── application_controller.rb # Shared data setup
│ ├── dashboard_controller.rb # Example authenticated page
│ ├── home_controller.rb # Public landing page
│ ├── sessions_controller.rb # Login/logout
│ ├── users_controller.rb # Registration
│ ├── identity/ # Password reset
│ └── settings/ # User settings
├── frontend/
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── nav-main.tsx # Main navigation
│ │ ├── app-sidebar.tsx # Sidebar component
│ │ └── user-menu-content.tsx # User dropdown
│ ├── hooks/
│ │ ├── use-flash.tsx # Flash message hook
│ │ └── use-appearance.tsx # Dark mode hook
│ ├── layouts/
│ │ ├── app-layout.tsx # Main app layout
│ │ ├── auth-layout.tsx # Auth pages layout
│ │ └── app/
│ │ ├── app-sidebar-layout.tsx
│ │ └── app-header-layout.tsx
│ ├── pages/
│ │ ├── dashboard/index.tsx # Dashboard page
│ │ ├── home/index.tsx # Landing page
│ │ ├── sessions/new.tsx # Login page
│ │ ├── users/new.tsx # Registration page
│ │ └── settings/ # Settings pages
│ └── types/
│ └── index.ts # Shared TypeScript types
Adding a New Resource
1. Generate the controller:
bin/rails generate controller Products index show new create edit update destroy
2. Create the page components:
// app/frontend/pages/products/index.tsx
import { Head, Link } from '@inertiajs/react'
import AppLayout from '@/layouts/app-layout'
import { Button } from '@/components/ui/button'
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table'
interface Product {
id: number
name: string
price: number
}
interface Props {
products: Product[]
}
export default function ProductsIndex({ products }: Props) {
return (
Products
Add Product
Name
Price
Actions
{products.map((product) => (
{product.name}
${product.price}
Edit
))}
)
}
3. Update navigation:
// app/frontend/components/nav-main.tsx
const navItems = [
{ title: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
{ title: 'Products', href: '/products', icon: Package }, // Add this
// ...
]
4. Add route:
# config/routes.rb
resources :products
Adding New shadcn/ui Components
The starter kit includes many components, but you can add more:
# Add a specific component
npx shadcn@latest add toast
npx shadcn@latest add calendar
npx shadcn@latest add data-table
# See all available components
npx shadcn@latest add
Customizing the Layout
Switch between sidebar and header layouts:
// app/frontend/layouts/app-layout.tsx
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout'
import AppHeaderLayout from '@/layouts/app/app-header-layout'
// Use sidebar (default)
export default function AppLayout({ children }: Props) {
return {children}
}
// Or use header layout
export default function AppLayout({ children }: Props) {
return {children}
}
Extending Types
// app/frontend/types/index.ts
export interface User {
id: number
name: string
email: string
avatar_url: string | null
}
// Add your own types
export interface Product {
id: number
name: string
description: string
price: number
created_at: string
}
export interface PageProps {
auth: {
user: User | null
}
flash: {
success?: string
error?: string
}
}
Using the Flash Hook
The starter kit includes a flash message system with Sonner toasts:
// Already set up in the layout, just use flash in your controller
class ProductsController Dashboard content
}
// Pass static props to the layout
Dashboard.layout = [AppLayout, { title: 'Dashboard', breadcrumbs: ['Home', 'Dashboard'] }]
Dynamic Layout Props
import { useLayoutProps } from '@inertiajs/react'
import AppLayout from '@/layouts/app-layout'
export default function UserProfile({ user }) {
// Set layout props dynamically
useLayoutProps({ title: user.name, breadcrumbs: ['Users', user.name] })
return {user.name}'s profile
}
UserProfile.layout = AppLayout
In the Layout
// app/frontend/layouts/app-layout.tsx
import { useLayoutProps } from '@inertiajs/react'
export default function AppLayout({ children }) {
const { title, breadcrumbs } = useLayoutProps()
return (
{title}
{breadcrumbs?.join(' > ')}
{children}
)
}
Inertia Modal - Render Pages as Dialogs
The inertia_rails-contrib gem and @inertiaui/modal package let you render any Inertia page as a modal dialog.
Installation
# Ruby gem (optional, for base_url helper)
bundle add inertia_rails-contrib
# NPM package (Vue)
npm install @inertiaui/modal-vue
# NPM package (React)
npm install @inertiaui/modal-react
Setup (React)
// app/frontend/entrypoints/application.js
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { renderApp } from '@inertiaui/modal-react'
createInertiaApp({
resolve: (name) => {
const pages = import.meta.glob('../pages/**/*.tsx', { eager: true })
return pages[`../pages/${name}.tsx`]
},
setup({ el, App, props }) {
createRoot(el).render(renderApp(App, props))
},
})
Tailwind Configuration
// tailwind.config.js (v3)
module.exports = {
content: [
// ... your content paths
'./node_modules/@inertiaui/modal-react/src/**/*.{js,jsx,ts,tsx}',
],
}
/* For Tailwind v4 */
@import "tailwindcss";
@source '../../../node_modules/@inertiaui/modal-react';
Basic Usage
Open a page as modal:
import { ModalLink } from '@inertiaui/modal-react'
export default function UsersList() {
return (
Create User
)
}
Wrap page content in Modal:
// pages/users/create.tsx
import { Modal } from '@inertiaui/modal-react'
export default function CreateUser({ roles }) {
return (
Create User
)
}
Modal with Base URL
Enable URL updates and browser history:
Controller:
class UsersController
Create User
Now the URL changes to /users/create when opened, supports browser back button, and can be bookmarked.
Slideover Variant
User Details
{/* Content slides in from the side */}
Nested Modals
Edit User
{/* Open another modal from within */}
Add New Role
Closing Modals
import { Modal } from '@inertiaui/modal-react'
export default function EditUser({ onClose }) {
return (
Cancel
)
}
Integrating shadcn/ui
Use shadcn/ui components with Inertia Rails for a polished UI.
Setup (React)
# Initialize shadcn/ui
npx shadcn@latest init
# Add components
npx shadcn@latest add button input card form
Form with shadcn/ui
import { useForm } from '@inertiajs/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export default function LoginForm() {
const { data, setData, post, processing, errors } = useForm({
email: '',
password: '',
})
function submit(e) {
e.preventDefault()
post('/login')
}
return (
Login
Email
setData('email', e.target.value)}
placeholder="you@example.com"
/>
{errors.email && (
{errors.email}
)}
Password
setData('password', e.target.value)}
/>
{processing ? 'Signing in...' : 'Sign in'}
)
}
Data Table with Sorting and Filtering
import { router, usePage } from '@inertiajs/react'
import { useState, useCallback } from 'react'
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Link } from '@inertiajs/react'
import { useDebouncedCallback } from 'use-debounce'
export default function UsersTable({ users, filters }) {
const [search, setSearch] = useState(filters.search || '')
const [sort, setSort] = useState(filters.sort || 'name')
const [direction, setDirection] = useState(filters.direction || 'asc')
const debouncedSearch = useDebouncedCallback((value) => {
router.get('/users', { search: value, sort, direction }, {
preserveState: true,
replace: true,
})
}, 300)
function toggleSort(column) {
const newDirection = sort === column && direction === 'asc' ? 'desc' : 'asc'
setSort(column)
setDirection(newDirection)
router.get('/users', { search, sort: column, direction: newDirection }, {
preserveState: true,
replace: true,
})
}
return (
{
setSearch(e.target.value)
debouncedSearch(e.target.value)
}}
placeholder="Search users..."
className="max-w-sm"
/>
toggleSort('name')}>
Name {sort === 'name' && (direction === 'asc' ? '↑' : '↓')}
toggleSort('email')}>
Email {sort === 'email' && (direction === 'asc' ? '↑' : '↓')}
Actions
{users.map((user) => (
{user.name}
{user.email}
Edit
))}
)
}
Search with Filters
Controller
class UsersController applyFilters(), 300)
function clearFilters() {
setSearch('')
setRole('')
setActive('')
router.get('/users', {}, { preserveState: true, replace: true })
}
return (
{ setSearch(e.target.value); debouncedSearch() }}
placeholder="Search..."
className="input"
/>
{ setRole(e.target.value); applyFilters({ role: e.target.value }) }} className="select">
All Roles
Admin
User
{ setActive(e.target.value); applyFilters({ active: e.target.value }) }} className="select">
All Status
Active
Inactive
Clear
)
}
Multi-Step Wizard
Controller
class OnboardingController
{/* Progress indicator */}
{Array.from({ length: total_steps }, (_, i) => i + 1).map((i) => (
{i}
))}
{typeof children === 'function' ? children({ form }) : children}
{step > 1 && (
Back
)}
{step === total_steps ? 'Complete' : 'Next'}
)
}
Flash Messages with Toast
Shared Data Setup
# app/controllers/application_controller.rb
class ApplicationController {
{
success: flash.notice,
error: flash.alert,
info: flash[:info],
warning: flash[:warning]
}.compact
}
end
Toast Component (React)
// components/FlashMessages.tsx
import { usePage } from '@inertiajs/react'
import { useEffect, useState } from 'react'
interface Toast {
id: number
type: string
message: string
}
export default function FlashMessages() {
const { flash } = usePage().props
const [toasts, setToasts] = useState([])
useEffect(() => {
if (!flash) return
Object.entries(flash).forEach(([type, message]) => {
if (message) {
const id = Date.now() + Math.random()
setToasts((prev) => [...prev, { id, type, message: message as string }])
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, 5000)
}
})
}, [flash])
const colorMap: Record = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
info: 'bg-blue-500 text-white',
warning: 'bg-yellow-500 text-black',
}
return (
{toasts.map((toast) => (
{toast.message}
))}
)
}
Usage in Layout
// layouts/AppLayout.tsx
import FlashMessages from '@/components/FlashMessages'
export default function AppLayout({ children }) {
return (
{/* ... */}
{children}
)
}
Flash API (v3)
Inertia.js v3 introduces a dedicated Flash API for richer flash data beyond simple key-value strings.
Server-Side
class PostsController {
router.post(flash.undo.url)
})
}
}
// Client-side flash (no server roundtrip)
router.flash({ notice: 'Saved locally' })
Configure Flash Keys
InertiaRails.configure do |config|
config.flash_keys = %i[notice alert success error warning info]
end
Confirmation Dialogs
Reusable Confirm Component
// components/ConfirmDialog.tsx
import { useState, useCallback, useRef } from 'react'
interface ConfirmOptions {
title?: string
message?: string
confirmText?: string
cancelText?: string
destructive?: boolean
}
export function useConfirm() {
const [isOpen, setIsOpen] = useState(false)
const [options, setOptions] = useState({})
const resolveRef = useRef void) | null>(null)
const confirm = useCallback((opts: ConfirmOptions = {}) => {
setOptions({
title: 'Are you sure?',
message: 'This action cannot be undone.',
confirmText: 'Confirm',
cancelText: 'Cancel',
destructive: false,
...opts,
})
setIsOpen(true)
return new Promise((resolve) => {
resolveRef.current = resolve
})
}, [])
function handleConfirm() {
setIsOpen(false)
resolveRef.current?.(true)
}
function handleCancel() {
setIsOpen(false)
resolveRef.current?.(false)
}
const ConfirmDialog = isOpen ? (
{options.title}
{options.message}
{options.cancelText}
{options.confirmText}
) : null
return { confirm, ConfirmDialog }
}
Usage
import { router } from '@inertiajs/react'
import { useConfirm } from '@/components/ConfirmDialog'
export default function UserRow({ user }) {
const { confirm, ConfirmDialog } = useConfirm()
async function deleteUser() {
const confirmed = await confirm({
title: 'Delete User',
message: `Are you sure you want to delete ${user.name}?`,
confirmText: 'Delete',
destructive: true,
})
if (confirmed) {
router.delete(`/users/${user.id}`)
}
}
return (
Delete
{ConfirmDialog}
)
}
Handling Rails Validation Error Types
Rails returns different error formats. Handle
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: cole-robertson
- Source: cole-robertson/inertia-rails-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.