Loading...
Loading...
ALWAYS `render inertia: { key: data }` to pass data as props — instance variables are NOT auto-passed (only alba-inertia does that). Rails controller patterns for Inertia.js: render inertia, prop types (defer, optional, merge, scroll), shared data, flash, PRG redirects, validation errors. Use when writing controllers that load data, display records, or serve Inertia responses. CRITICAL: external URLs (Stripe/OAuth) MUST use inertia_location, NEVER redirect_to.
npx skill4agent add inertia-rails/skills inertia-rails-controllersinertia_shareInertiaControllerInertiaRails.deferInertiaRails.optionalInertiaRails.onceredirect_toinertia_locationX-Inertia-Locationerrors.full_messageserrors.to_hash(true)inertia.deferInertia.deferinertia_rails.deferInertiaRails.defer { ... }InertiaRailsalba-inertiarender inertia: { key: data }successerrorconfig.flash_keysnoticealertFlashDataTRAP: This setting only auto-infers the component name from controller/action — it does NOT auto-pass instance variables as props. Writingdefault_render: truein an action with@posts = Post.allrenders the correct component but sends zero data to the frontend. Instance variables are only auto-serialized as props whendefault_render: truegem is configured — checkalba-inertiabefore relying on this. Without it, you MUST useGemfileto pass any data to the page.render inertia: { posts: data }Empty actions () are correct ONLY for pages that need no data (e.g., a static dashboard page, a login form). If the action queries the database, it MUST calldef index; endwith data.render inertia:
| Situation | Syntax | Component path |
|---|---|---|
| Action loads data | | Inferred from controller/action |
| Action loads NO data (static page) | Empty action or | Inferred from controller/action |
| Rendering a different page | | Explicit path |
render inertia:inertia_share# CORRECT — data passed as props
def index
render inertia: { users: users_data, stats: InertiaRails.defer { ExpensiveQuery.run } }
end
# CORRECT — static page, no data needed
def index; end
# WRONG — @posts is NEVER sent to the frontend (without alba-inertia)
def index
@posts = Post.all
endNote: If the project uses thegem (checkalba-inertia), instance variables are auto-serialized as props and explicitGemfileis not needed. See therender inertia:skill for that convention.alba-inertia
InertiaRails.deferinertia.deferInertia.deferInertiaRails| Type | Syntax | Behavior |
|---|---|---|
| Regular | | Always evaluated, always included |
| Lazy | | Included on initial page render, lazily evaluated on partial reloads |
| Optional | | Only evaluated on partial reload requesting it |
| Defer | | Loaded after initial page render |
| Defer (grouped) | | Grouped deferred — fetched in parallel |
| Once | | Resolved once, remembered across navigations |
| Merge | | Appended to existing array (infinite scroll) |
| Deep merge | | Deep merged into existing object |
| Always | | Included even in partial reloads |
| Scroll | | Scroll-aware prop for infinite scroll |
def index
render inertia: {
filters: filter_params,
messages: -> { messages_scope.as_json },
stats: InertiaRails.defer { Dashboard.stats },
chart: InertiaRails.defer(group: 'analytics') { Dashboard.chart },
countries: InertiaRails.once { Country.pluck(:name, :code) },
posts: InertiaRails.merge { @posts.as_json },
csrf: InertiaRails.always { form_authenticity_token },
}
end# Controller
def show
render inertia: {
basic_stats: Stats.quick_summary,
analytics: InertiaRails.defer { Analytics.compute_slow },
}
end// Page component — child reads deferred prop from page props
import { Deferred, usePage } from '@inertiajs/react'
export default function Dashboard({ basic_stats }: Props) {
return (
<>
<QuickStats data={basic_stats} />
<Deferred data="analytics" fallback={<div>Loading analytics...</div>}>
<AnalyticsPanel />
</Deferred>
</>
)
}
function AnalyticsPanel() {
const { analytics } = usePage<{ analytics: Analytics }>().props
return <div>{analytics.revenue}</div>
}inertia_sharecurrent_userconfig.*class ApplicationController < ActionController::Base
# Static
inertia_share app_name: 'MyApp'
# Using lambdas (most common)
inertia_share auth: -> { { user: current_user&.as_json(only: [:id, :name, :email, :role]) } }
# Conditional
inertia_share if: :user_signed_in? do
{ notifications: -> { current_user.unread_notifications_count } }
end
endreferences/configuration.mdinertia_share# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.flash_keys = %i[notice alert toast] # default: %i[notice alert]
endredirect_to users_path, notice: "User created!"
# or
flash.alert = "Something went wrong"
redirect_to users_pathredirect_todef create
@user = User.new(user_params)
if @user.save
redirect_to users_path, notice: "Created!"
else
redirect_back_or_to new_user_path, inertia: { errors: @user.errors.to_hash(true) }
end
endto_hashto_hash(true)to_hash{ name: ["can't be blank"] }to_hash(true){ name: ["Name can't be blank"] }nameerrors.full_messagescaninertia-rails-controllersinertia-rails-pagesreferences/authorization.mdcaninertia_locationredirect_toinertia_locationX-Inertia-Locationwindow.location = url# Stripe checkout — MUST use inertia_location, not redirect_to
def create
checkout_session = Current.user.payment_processor.checkout(
mode: "payment",
line_items: "price_xxx",
success_url: enrollments_url,
cancel_url: course_url(@course),
)
inertia_location checkout_session.url
endinertia_locationconfig.encrypt_history = Rails.env.production?redirect_to path, inertia: { clear_history: true }references/configuration.mdreferences/configuration.mdInertiaRails.configure| Symptom | Cause | Fix |
|---|---|---|
| 302 loop on Stripe/OAuth redirect | | Use |
| Errors don't display next to fields | Error keys don't match input | |
TS2305: | js-routes not regenerated after adding routes | Run |
inertia-rails-formsinertia-rails-pagesshadcn-inertiainertia-rails-pages<Deferred>inertia-rails-typescriptalba-inertiainertia-rails-testingmergescrolldeep_mergereferences/prop-types.mdprop-types.mddeferoptionaloncealwaysreferences/configuration.mdInertiaRails.configure