AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Inertia Rails Cookbook

skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook · by cole-robertson

Recipes and patterns for common Inertia Rails use cases. Includes modal dialogs, shadcn/ui integration, search with filters, wizard flows, and other advanced patterns.

No reviews yet
0 installs
3 views
0.0% view→install

Install

$ agentstack add skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-cole-robertson-inertia-rails-skills-inertia-rails-cookbook)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Inertia Rails Cookbook? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.