InertiaJb
InertiaJb lets you declare Inertia.js props for your Rails frontend components inside view templates, using plain Ruby Hashes.
It is built on top of the inertia-rails gem.
A *.inertia template is just Ruby code whose last expression is a Hash.
inertia is registered as a real Rails template format, so the template renders
through the ordinary Rails pipeline and its resulting Hash is handed straight to
InertiaRails::Renderer — meaning every Inertia protocol feature works out of
the box: partial reloads (at any nesting depth), optional / always / deferred /
scroll props, prop merging, shared data, and global key transforms.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
# Shared props are merged in automatically.
inertia_share user: -> { Current.user }
def show
@message = Message.find(params[:id])
end
end
# app/views/messages/show.inertia
{
message: {
content: @message.content,
author: {
name: @message..name,
email: @message..email,
url: url_for(@message., format: :json)
},
comments: render(partial: "comments/comment", collection: @message.comments)
}
}
// app/javascript/pages/messages/show.jsx
export default function Message({ user, message }) {
return (
<div>
<p>Hi, {user.name}</p>
<p>{message.content}</p>
<a href={message.author.url}>
{message.author.name} <{message.author.email}>
</a>
{message.comments.map((c) => <Comment key={c.id} comment={c} />)}
</div>
);
}
Installation
Add the gem to your Gemfile:
gem "inertia_jb"
Then, in your inertia-rails initializer, keep default_render off (that is
its own default) so a normal implicit render falls through to your .inertia
template:
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.default_render = false
end
Layout
On an initial (non-XHR) page load the data-page root element is wrapped in
a layout; Inertia (XHR) visits always return a bare JSON body with no layout.
The layout is chosen from inertia-rails' config.layout, matching
InertiaRails::Renderer's own semantics.
Templates and partials
- Page templates live at
app/views/<controller>/<action>.inertiaand must return a Hash (your Inertia props). - Partials are ordinary jb partials named
_name.jb. They return a Hash, and compose naturally:
# app/views/messages/show.inertia
{
author: render(partial: "authors/author", object: @message.),
comments: render(partial: "comments/comment", collection: @message.comments)
}
# app/views/authors/_author.jb
{ id: .id, name: .name }
render(partial:, collection:) returns an array of Hashes (thanks to jb),
which you embed directly. Don't name partials .inertia — that format is for
top-level page templates only, and turns the result into an Inertia response.
Sharing a partial with a plain JSON API
An Inertia page and a plain JSON endpoint are both, in the end, just a Hash,
so a single jb partial can back both. Name the partial without a format
(_message.jb, not _message.html.jb) so it resolves for the formats an Inertia
page renders under and the json format a normal API request uses:
# app/views/messages/_message.jb
{
id: .id,
content: .content,
author: render(partial: "authors/author", object: .)
}
# app/views/messages/show.inertia — the Inertia page
render(partial: "messages/message", object: @message)
# app/views/messages/index.inertia — nested under a key
{ messages: render(partial: "messages/message", collection: @messages, as: :message) }
# app/views/messages/show.json.jb — a plain JSON endpoint
render(partial: "messages/message", object: @message)
Note the asymmetry: the JSON endpoint can return that Array at the top level,
but the Inertia page must nest it under a key ({ messages: … }) — Inertia
props must be an object, never a top-level Array. An object partial is fine
bare, as in show.inertia above.
If you'd rather keep a format-specific partial (_message.json.jb), borrow the
:json variant from the Inertia side with formats::
# app/views/messages/show.inertia
render(partial: "messages/message", object: @message, formats: [:json])
Inertia prop types
Because props are just a Hash, Inertia's special prop types are plain values you
drop in. Inside a .inertia template you can use the short helpers
(optional, always, defer, scroll, merge, deep_merge, once, cache) or the full
InertiaRails.* methods.
{
id: @post.id,
title: @post.title,
# Only fetched when explicitly requested in a partial reload.
stats: optional { @post.expensive_stats },
# Always fetched, even if not requested in a partial reload.
settings: always { current_settings },
# Excluded from the initial load; fetched in a follow-up request.
comments: defer(group: :comments) {
render(partial: "comments/comment", collection: @post.comments)
},
# Infinite scrolling (accepts a paginator or explicit metadata).
feed: scroll(@pagy) {
render(partial: "feed/item", collection: @items)
},
# Sent once and cached client-side; skipped on later visits until reset.
flash: once { session.delete(:flash) },
# Server-side cached via Rails.cache; the block's JSON output is reused
# across requests until the cache entry expires.
report: cache("posts/#{@post.id}/report", expires_in: 5.minutes) {
render(partial: "reports/report", object: @post.report)
}
}
See the inertia-rails docs for partial reloads, deferred props, infinite scroll, once props, and prop caching.
camelCase keys
If you configure inertia-rails' prop_transformer to camelize keys, it applies
at every nesting depth — because your props reach inertia-rails as a real
Hash:
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.default_render = false
config.prop_transformer = ->(props:) { props.deep_transform_keys { |k| k.to_s.camelize(:lower) } }
end
Alternatively, just write camelCase keys directly in your templates.
Notes
- Inertia props must be an object, so page templates should return a Hash (not a top-level Array).
- The gem registers
inertiaas aMime::Typealias oftext/html. Because it is an alias, it is skipped by content negotiation:inertiais a template format, never a wire format, andAcceptheaders are unaffected. - If both
show.html.erbandshow.inertiaexist for one action, Rails prefers the exact format match — the ERB one. - If your app pins
config.action_view.default_formats, you don't need to addinertiato it: page templates carry no format extension, so they resolve under whatever format the request already asked for.
Upgrading from 0.3.x
- Rename page templates:
show.html.inertia→show.inertia. A leftover.html.inertiatemplate raises with the rename instruction, so nothing fails silently. Jb partials (_author.html.jb,_author.jb) don't change. - Component names now follow the rendered template rather than
controller_path/action_name. Identical for a normal implicit render; it differs when yourender :other_action(you now getother_action's component, which is the point) or when a template is inherited from a parent controller's view directory (the component follows the template's directory). { **render(partial: …) }in a page template can lose the wrapper — a barerender(partial: …)is now read as props.
config.default_render = false is still required, unchanged from 0.3.x.
Development
bundle install
bundle exec rake test
License
MIT. See LICENSE.