mcpField
I
inertia-rails-cookbook

inertia-rails-cookbook

github.com/cole-robertsonDevelopment3887 installsUpdated 30w ago
Repository

About inertia-rails-cookbook

Inertia Rails Cookbook is a recipe collection for developers building Inertia Rails applications. It covers common application patterns, frontend integrations, and Rails workflows with practical examples.

Key features

  1. 01Resource and route setup
  2. 02Layout props and customization
  3. 03Modal pages and dialogs
  4. 04Search, filters, and wizards
  5. 05Flash messages and validation
  6. 06ActionCable and file downloads

Use cases

  1. 01Add a resource page to a Rails application
  2. 02Build searchable, filterable data tables
  3. 03Create multi-step onboarding flows
  4. 04Show pages in modals with URL history

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/cole-robertson/inertia-rails-skills --skill inertia-rails-cookbook

After installing, type/inertia-rails-cookbookin Claude Code to confirm it loaded.

Details

From the project README

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 (
    <AppLayout>
      <Head title="Products" />

      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">Products</h1>
        <Button asChild>
          <Link href="/products/new">Add Product</Link>
        </Button>
      </div>

      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Name</TableHead>
            <TableHead>Price</TableHead>
            <TableHead>Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {products.map((product) => (
            <TableRow key={product.id}>
              <TableCell>{product.name}</TableCell>
              <TableCell>${product.price}</TableCell>
              <TableCell>
                <Link href={`/products/${product.id}/edit`}>Edit</Link>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </AppLayout>
  )
}

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 <AppSidebarLayout>{children}</AppSidebarLayout>
}

// Or use header layout
export default function AppLayout({ children }: Props) {
  return <AppHeaderLayout>{children}</AppHeaderLayout>
}
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 < ApplicationController
  def create
    @product = Product.create(product_params)
    redirect_to products_path, notice: 'Product created!'
  end
end

The use-flash hook automatically displays flash messages as toasts.

Removing Features You Don't Need

Remove settings pages:

rm -rf app/frontend/pages/settings
rm -rf app/controllers/settings
# Remove routes in config/routes.rb

Remove authentication (for internal tools):

rm -rf app/frontend/pages/sessions
rm -rf app/frontend/pages/users
rm -rf app/frontend/pages/identity
rm app/controllers/sessions_controller.rb
rm app/controllers/users_controller.rb
rm -rf app/controllers/identity
# Update routes and ApplicationController

Layout Props (v3)

Share data between pages and their persistent layouts using useLayoutProps:

Static Layout Props
// app/frontend/pages/dashboard/index.tsx
import AppLayout from '@/layouts/app-layout'

export default function Dashboard({ stats }) {
  return <div>Dashboard content</div>
}

// 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 <div>{user.name}'s profile</div>
}

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 (
    <div>
      <header>
        <h1>{title}</h1>
        <nav>{breadcrumbs?.join(' > ')}</nav>
      </header>
      <main>{children}</main>
    </div>
  )
}

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 (
    <ModalLink href="/users/create">
      Create User
    </ModalLink>
  )
}

Wrap page content in Modal:

// pages/users/create.tsx
import { Modal } from '@inertiaui/modal-react'

export default function CreateUser({ roles }) {
  return (
    <Modal>
      <h2>Create User</h2>
      <UserForm roles={roles} />
    </Modal>
  )
}
Modal with Base URL

Enable URL updates and browser history:

Controller:

class UsersController < ApplicationController
  def create
    render inertia_modal: {
      roles: Role.all.as_json
    }, base_url: users_path
  end
end

Link with navigation:

<ModalLink href="/users/create" navigate>
  Create User
</ModalLink>

Now the URL changes to /users/create when opened, supports browser back button, and can be bookmarked.

Slideover Variant
<Modal slideover>
  <h2>User Details</h2>
  {/* Content slides in from the side */}
</Modal>
Nested Modals
<Modal>
  <h2>Edit User</h2>
  <UserForm />

  {/* Open another modal from within */}
  <ModalLink href="/roles/create">
    Add New Role
  </ModalLink>
</Modal>
Closing Modals
import { Modal } from '@inertiaui/modal-react'

export default function EditUser({ onClose }) {
  return (
    <Modal onClose={onClose}>
      <button onClick={onClose}>Cancel</button>
    </Modal>
  )
}

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 (
    <Card className="w-[400px]">
      <CardHeader>
        <CardTitle>Login</CardTitle>
      </CardHeader>
      <CardContent>
        <form onSubmit={submit} className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              value={data.email}
              onChange={(e) => setData('email', e.target.value)}
              placeholder="[email protected]"
            />
            {errors.email && (
              <p className="text-sm text-red-500">{errors.email}</p>
            )}
          </div>

          <div className="space-y-2">
            <Label htmlFor="password">Password</Label>
            <Input
              id="password"
              type="password"
              value={data.password}
              onChange={(e) => setData('password', e.target.value)}
            />
          </div>

          <Button type="submit" disabled={processing} className="w-full">
            {processing ? 'Signing in...' : 'Sign in'}
          </Button>
        </form>
      </CardContent>
    </Card>
  )
}
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 (
    <div className="space-y-4">
      <Input
        value={search}
        onChange={(e) => {
          setSearch(e.target.value)
          debouncedSearch(e.target.value)
        }}
        placeholder="Search users..."
        className="max-w-sm"
      />

      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>
              <Button variant="ghost" onClick={() => toggleSort('name')}>
                Name {sort === 'name' && (direction === 'asc' ? '↑' : '↓')}
              </Button>
            </TableHead>
            <TableHead>
              <Button variant="ghost" onClick={() => toggleSort('email')}>
                Email {sort === 'email' && (direction === 'asc' ? '↑' : '↓')}
              </Button>
            </TableHead>
            <TableHead>Actions</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {users.map((user) => (
            <TableRow key={user.id}>
              <TableCell>{user.name}</TableCell>
              <TableCell>{user.email}</TableCell>
              <TableCell>
                <Link href={`/users/${user.id}/edit`}>Edit</Link>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

Search with Filters

Controller
class UsersController < ApplicationController
  def index
    users = User.all

    # Apply search
    if params[:search].present?
      users = users.where('name ILIKE ? OR email ILIKE ?',
        "%#{params[:search]}%", "%#{params[:search]}%")
    end

    # Apply filters
    users = users.where(role: params[:role]) if params[:role].present?
    users = users.where(active: params[:active]) if params[:active].present?

    # Apply sorting
    sort_column = %w[name email created_at].include?(params[:sort]) ? params[:sort] : 'name'
    sort_direction = params[:direction] == 'desc' ? 'desc' : 'asc'
    users = users.order("#{sort_column} #{sort_direction}")

    # Paginate
    users = users.page(params[:page]).per(20)

    render inertia: {
      users: users.as_json(only: [:id, :name, :email, :role, :active]),
      filters: {
        search: params[:search],
        role: params[:role],
        active: params[:active],
        sort: sort_column,
        direction: sort_direction,
      },
      pagination: {
        current_page: users.current_page,
        total_pages: users.total_pages,
        total_count: users.total_count,
      }
    }
  end
end
Frontend with URL Sync
import { router } from '@inertiajs/react'
import { useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'

export default function UsersIndex({ users, filters, pagination }) {
  const [search, setSearch] = useState(filters.search || '')
  const [role, setRole] = useState(filters.role || '')
  const [active, setActive] = useState(filters.active || '')

  function applyFilters(overrides = {}) {
    router.get('/users', {
      search: search || undefined,
      role: role || undefined,
      active: active || undefined,
      ...overrides,
    }, {
      preserveState: true,
      replace: true,
    })
  }

  const debouncedSearch = useDebouncedCallback(() => applyFilters(), 300)

  function clearFilters() {
    setSearch('')
    setRole('')
    setActive('')
    router.get('/users', {}, { preserveState: true, replace: true })
  }

  return (
    <div className="space-y-4">
      <div className="flex gap-4">
        <input
          value={search}
          onChange={(e) => { setSearch(e.target.value); debouncedSearch() }}
          placeholder="Search..."
          className="input"
        />

        <select value={role} onChange={(e) => { setRole(e.target.value); applyFilters({ role: e.target.value }) }} className="select">
          <option value="">All Roles</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
        </select>

        <select value={active} onChange={(e) => { setActive(e.target.value); applyFilters({ active: e.target.value }) }} className="select">
          <option value="">All Status</option>
          <option value="true">Active</option>
          <option value="false">Inactive</option>
        </select>

        <button onClick={clearFilters}>Clear</button>
      </div>

      <UserTable users={users} />
      <Pagination pagination={pagination} />
    </div>
  )
}

Multi-Step Wizard

Controller
class OnboardingController < ApplicationController
  def show
    step = params[:step]&.to_i || 1

    render inertia: "onboarding/step#{step}", props: {
      step: step,
      total_steps: 4,
      data: session[:onboarding] || {}
    }
  end

  def update
    step = params[:step].to_i

    # Merge step data into session
    session[:onboarding] ||= {}
    session[:onboarding].merge!(step_params.to_h)

    if step < 4
      redirect_to onboarding_path(step: step + 1)
    else
      # Complete onboarding
      User.create!(session[:onboarding])
      session.delete(:onboarding)
      redirect_to dashboard_path, notice: 'Welcome!'
    end
  end

  private

  def step_params
    case params[:step].to_i
    when 1 then params.permit(:name, :email)
    when 2 then params.permit(:company, :role)
    when 3 then params.permit(:preferences)
    when 4 then params.permit(:terms_accepted)
    end
  end
end
Wizard Component
import { useForm, router } from '@inertiajs/react'

export default function WizardStep({ step, total_steps, data, children }) {
  const form = useForm(data)

  function next(e) {
    e.preventDefault()
    form.post(`/onboarding?step=${step}`)
  }

  function back() {
    router.get(`/onboarding?step=${step - 1}`)
  }

  return (
    <div>
      {/* Progress indicator */}
      <div className="flex gap-2 mb-8">
        {Array.from({ length: total_steps }, (_, i) => i + 1).map((i) => (
          <div
            key={i}
            className={`w-8 h-8 rounded-full flex items-center justify-center ${
              i <= step ? 'bg-blue-500 text-white' : 'bg-gray-200'
            }`}
          >
            {i}
          </div>
        ))}
      </div>

      <form onSubmit={next}>
        {typeof children === 'function' ? children({ form }) : children}

        <div className="flex gap-4 mt-8">
          {step > 1 && (
            <button type="button" onClick={back}>Back</button>
          )}
          <button type="submit" disabled={form.processing}>
            {step === total_steps ? 'Complete' : 'Next'}
          </button>
        </div>
      </form>
    </div>
  )
}

Flash Messages with Toast

Shared Data Setup
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  inertia_share flash: -> {
    {
      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<Toast[]>([])

  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<string, string> = {
    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 (
    <div className="fixed top-4 right-4 space-y-2 z-50">
      {toasts.map((toast) => (
        <div
          key={toast.id}
          className={`px-4 py-3 rounded-lg shadow-lg transition-all ${colorMap[toast.type] || 'bg-gray-500 text-white'}`}
        >
          {toast.message}
        </div>
      ))}
    </div>
  )
}
Usage in Layout
// layouts/AppLayout.tsx
import FlashMessages from '@/components/FlashMessages'

export default function AppLayout({ children }) {
  return (
    <div>
      <FlashMessages />
      <nav>{/* ... */}</nav>
      <main>{children}</main>
    </div>
  )
}

Flash API (v3)

Inertia.js v3 introduces a dedicated Flash API for richer flash data beyond simple key-value strings.

Server-Side
class PostsController < ApplicationController
  def create
    post = Post.create!(post_params)

    # Standard Rails flash (exposed via flash_keys config)
    redirect_to posts_path, notice: 'Post created!'
  end

  def update
    post = Post.find(params[:id])

    # Rich flash data via flash.inertia
    flash.inertia[:undo] = { url: undo_post_path(post), expires_in: 30 }

    # Current-request-only flash
    flash.now.inertia[:notification] = { type: 'info', message: 'Saving...' }

    redirect_to post_path(post)
  end
end
Frontend
import { usePage, router } from '@inertiajs/react'

function FlashHandler() {
  const { flash } = usePage().props

  // Access standard flash
  if (flash.notice) showToast(flash.notice)

  // Access rich flash data
  if (flash.undo) {
    showUndoToast(flash.undo.message, () => {
      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<ConfirmOptions>({})
  const resolveRef = useRef<((value: boolean) => 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<boolean>((resolve) => {
      resolveRef.current = resolve
    })
  }, [])

  function handleConfirm() {
    setIsOpen(false)
    resolveRef.current?.(true)
  }

  function handleCancel() {
    setIsOpen(false)
    resolveRef.current?.(false)
  }

  const ConfirmDialog = isOpen ? (
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      <div className="absolute inset-0 bg-black/50" onClick={handleCancel} />
      <div className="relative bg-white rounded-lg p-6 max-w-md w-full mx-4">
        <h3 className="text-lg font-semibold">{options.title}</h3>
        <p className="mt-2 text-gray-600">{options.message}</p>
        <div className="mt-6 flex gap-3 justify-end">
          <button onClick={handleCancel} className="btn-secondary">
            {options.cancelText}
          </button>
          <button
            onClick={handleConfirm}
            className={options.destructive ? 'btn-danger' : 'btn-primary'}
          >
            {options.confirmText}
          </button>
        </div>
      </div>
    </div>
  ) : 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 (
    <div>
      <button onClick={deleteUser}>Delete</button>
      {ConfirmDialog}
    </div>
  )
}

Handling Rails Validation Error Types

Rails returns different error formats. Handle them consistently:

# Controller helper
def format_errors(model)
  model.errors.to_hash.transform_values { |messages| messages.first }
end

# Usage
redirect_to edit_user_url(user), inertia: { errors: format_errors(user) }
// Frontend - errors are now { field: 'message' } format
form.errors.email  // "can't be blank"
Nested Model Errors
# For nested attributes
def format_nested_errors(model)
  errors = {}

  model.errors.each do |error|
    key = error.attribute.to_s.gsub('.', '_')
    errors[key] = error.message
  end

  errors
end

Real-Time Features with ActionCable

Setup Turbo Streams Alternative
// channels/notifications_channel.js
import { router } from '@inertiajs/react'
import consumer from './consumer'

consumer.subscriptions.create('NotificationsChannel', {
  received(data) {
    if (data.reload) {
      router.reload({ only: ['notifications'] })
    }
  }
})
Controller Broadcast
class NotificationsController < ApplicationController
  def create
    notification = current_user.notifications.create!(notification_params)

    ActionCable.server.broadcast(
      "notifications_#{current_user.id}",
      { reload: true }
    )

    redirect_to notifications_path
  end
end

File Downloads

Triggering Downloads
def download
  report = Report.find(params[:id])

  # Return download URL as prop
  render inertia: {
    download_url: rails_blob_path(report.file, disposition: 'attachment')
  }
end
// Trigger download without navigation
function downloadFile(url) {
  window.location.href = url
}

// Or use inertia_location for non-Inertia responses
router.visit(url, { method: 'get' })
External Redirect for Downloads
def export
  # Generate file...
  inertia_location export_download_path(token: token)
end

Comments

Markdown supported