InertiaJb lets you declare Inertia.js props for your Rails frontend components inside view templates, using plain Ruby Hashes powered by jb.
It is built on top of the inertia-rails gem.
A *.html.inertia template is just Ruby code whose last expression is a Hash.
That Hash is handed straight to InertiaRails::Renderer, so 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.html.inertia
{
message: {
content: @message.content,
author: {
name: @message.author.name,
email: @message.author.email,
url: url_for(@message.author, 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>
);
}Inertia's server side is fundamentally about producing a Hash of props that
inertia-rails then resolves and serializes. jb templates are plain Ruby
Hashes, so there is zero impedance mismatch: no DSL to learn, no intermediate
representation, and the full power of Ruby for building collections and
conditionals.
Coming from
props_template? That gem streams JSON strings, which don't map cleanly onto Inertia's Hash-based resolver. jb's plain-Hash output is the natural fit, which is why this gem is built on it.
Add the gem to your Gemfile:
gem "inertia_jb"Then, in your inertia-rails initializer, disable default_render so that a
normal implicit render falls through to your .html.inertia template:
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.default_render = false
endYou do not need to call use_inertia_instance_props.
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:
# config/initializers/inertia_rails.rb
InertiaRails.configure do |config|
config.default_render = false
# config.layout = true # (default) use the controller's normal layout
# config.layout = "inertia" # use app/views/layouts/inertia.html.erb
# config.layout = false # no layout — render just the <div id="app"> root
endtrue/nil— the controller's default layout, resolved the normal Rails way (app/views/layouts/application.html.erb, or anylayout "..."declaration in the controller).- A String — that named layout (
app/views/layouts/<name>.html.erb). false— no layout at all; the response is only the<div id="app" data-page="…">root, so you provide<html>/<head>/asset tags elsewhere.
Because config.layout is scoped per controller in inertia-rails, you can also
set it on a single controller with inertia_config layout: "...". Plain
(non-Inertia) .html.erb actions in the same app keep their layout regardless
of this setting.
Note: this gem does not perform server-side rendering (SSR), so
inertia_ssr_headin your layout will always be empty.
- Page templates live at
app/views/<controller>/<action>.html.inertiaand must return a Hash (your Inertia props). - Partials are ordinary jb partials named
_name.html.jb. They return a Hash, and compose naturally:
# app/views/messages/show.html.inertia
{
author: render(partial: "authors/author", object: @message.author),
comments: render(partial: "comments/comment", collection: @message.comments)
}# app/views/authors/_author.html.jb
{ id: author.id, name: author.name }render(partial:, collection:) returns an array of Hashes (thanks to jb),
which you embed directly. Don't name partials .html.inertia — that extension
triggers the Inertia response wrapper and is only for top-level page templates.
Because props are just a Hash, Inertia's special prop types are plain values you
drop in. Inside a .html.inertia template you can use the short helpers
(optional, always, defer, scroll, merge, deep_merge) 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)
}
}See the inertia-rails docs for partial reloads, deferred props, and infinite scroll.
Use plain Rails caching — you're caching Ruby Hashes:
@posts.map do |post|
Rails.cache.fetch(post) { render(partial: "posts/post", object: post) }
endIf 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) } }
endAlternatively, just write camelCase keys directly in your templates.
- Inertia props must be an object, so page templates should return a Hash (not a top-level Array).
- Don't install this alongside
inertia-builder; both register an:inertiatemplate handler.
bundle install
bundle exec rake testMIT. See LICENSE.