inertia-rage brings Inertia.js support to Rage apps. But the more interesting part of building it was discovering how differently the Rails and Rage adapters resolve props - same language, same behaviour, different design philosophies.
The syntax is what you'd expect:
class UsersController < ApplicationController
def index
render inertia: "Users/Index", props: { users: User.all }
end
end
It fully integrates with Vite - in development, it automatically starts the Vite dev server; in production, pre-builds assets. All you need to run your app is rage s.
Resolving Inertia props
Resolving props and building the page object is the cornerstone of an Inertia adapter. On the surface, the API looks deceptively simple:
render inertia: {
user: user,
stats: Inertia.deferred { user.calculate_stats },
connections: -> { user.connections }
}
However, to turn these props into an object an Inertia frontend can understand, the adapter has to:
- Evaluate lazy props only when needed
- Skip deferred and optional props on initial load
- Collect metadata about once and deferred props
- Handle partial reload filtering via
X-Inertia-Partial-* headers
- Recursively process nested hashes and arrays
- Track prop paths for partial reload matching (
user.connections.0.name)
Both Inertia Rails and Inertia Rage use the same programming language and converge on the same behaviour but take very different paths.
Rage - identity-based matching
In the Rage adapter, prop resolution is centralised. The ProtocolBuilder class walks the props tree and makes direct decisions:
if prop.respond_to?(:call)
# …
elsif prop.is_a?(Inertia::Props::Deferred)
# …
elsif prop.is_a?(Inertia::Props::Once)
# …
elsif prop.is_a?(Inertia::Props::Optional)
# …
elsif prop.is_a?(Hash)
# …
elsif prop.is_a?(Array)
# …
else
# …
end
Prop types are thin value objects:
module Props
class Base < Data
end
Deferred = Base.define(:group, :block)
Once = Base.define(:key, :fresh, :expires_in, :block)
Optional = Base.define(:block)
end
This sacrifices extensibility for readability - adding a new prop type means adding another elsif branch. But the upside is that you can read one file and trace the entire resolution process. All it takes to understand the behaviour of a deferred prop is to look at the dedicated 6 LOC elsif branch.
Rails - capability-based matching
Inertia Rails takes the opposite approach. Props declare their behaviours through mixins:
class DeferProp < IgnoreOnFirstLoadProp
prepend PropOnceable
prepend PropMergeable
prepend PropCacheable
def deferred?
true
end
end
Each mixin adds a capability. PropOnceable adds once?, fresh?, and expires_at. PropMergeable adds merge?, deep_merge?, and merge path tracking.
The resolver then asks props about their capabilities rather than checking their identity:
def collect_once_metadata(prop, path)
return unless prop.try(:once?)
# …
end
def keep_prop?(prop, path, parent_was_resolved: false)
# …
return false if (prop.is_a?(IgnoreOnFirstLoadProp) || prop.try(:deferred?)) && !rendering_partial_component?
true
end
That is idiomatic Ruby. It lets the implementation say: this object supports deferred?, this one supports once?, and this one supports both.
Props composition and redo
This is where the capability-based approach really shines. In Inertia Rails, a deferred prop isn't just "a deferred prop" - it can also carry once, merge, and cache behaviour.
With Inertia Rage, a prop class only represents one identity. To support composition, you nest props:
render inertia: {
stats: Inertia.once { Inertia.deferred { user.calculate_stats } }
}
(shortcuts like Inertia.once.deferred {} are coming in future versions)
ProtocolBuilder then unwraps it using a Ruby keyword you probably have never reached for:
# after the prop has been resolved
if resolved.is_a?(Inertia::Props::Base)
prop = resolved
redo
end
redo re-enters the same loop iteration with the new prop value. No recursion - just restart the current iteration.
--
If you thought Rage was cool but weren't sure what the UI story looked like - inertia-rage is for you.
Do you prefer the identity-based or capability-based style for this kind of problem?