https://newsletter.masilotti.com/p/on-building-a-framework-agnostic
Joe Masilotti
Joe Masilotti
SubscribeSign in
On building a framework-agnostic Ruby gem (and making sure it doesn't
break)
What I learned supporting ERB, React, and Vue from a single Ruby gem.
And how I keep it from breaking with automated iOS tests.
Joe Masilotti's avatar
Joe Masilotti
Apr 01, 2026
7
1
Share
Ruby Native needs to work with ERB, React, and Vue. That's three
frameworks, three sets of conventions, and three ways developers
expect an API to feel. And every new feature needs to ship across all
of them without breaking the others.
This week I added a native navbar, and it was the first real test of
whether this approach scales. Here's what I've learned about keeping
an API clean across frameworks, and how I'm catching regressions.
It's HTML all the way down
When I started Ruby Native, it was ERB-only. The tab bar, push
notifications, and forms all used the same pattern: render a hidden
HTML element with data-native-* attributes. The native app detects
these "signal elements" via a MutationObserver and translates them
into real native UI.
For example, the native navbar is just this:
Hidden divs, that's it! The native app doesn't know what generated
them. It just reads the DOM.
This turned out to be the most important decision in the entire
project. Because when it came time to support React and Vue, I didn't
have to change anything on the native side. I just needed new ways to
produce the same HTML.
Each framework's API should feel good
ERB developers expect blocks and builders. React developers expect
components and props. If you force one framework's patterns onto
another, the API feels wrong even if it works.
Here's the same navbar in ERB:
<%= native_navbar_tag "Account" do |navbar| %>
<% navbar.button icon: "ellipsis.circle" do |menu| %>
<% menu.item "Edit profile", href: "/account/edit", icon: "pencil" %>
<% menu.item "Sign out", click: "#sign-out", icon: "rectangle.portrait" %>
<% end %>
<% end %>
And in React:
import { NativeNavbar, NativeButton, NativeMenuItem } from "ruby_native/react"
Different syntax. Same output. The ERB version uses Ruby's block
pattern with a builder that yields menu items. The React version uses
component composition with props. Both feel right in their own
context.
The React components themselves are intentionally thin. Here's
NativeButton:
export function NativeButton({ position = "trailing", icon, title, href, click, selected, children }) {
const props = { "data-native-button": true }
if (icon) props["data-native-icon"] = icon
if (title) props["data-native-title"] = title
if (href) props["data-native-href"] = href
if (click) props["data-native-click"] = click
if (position) props["data-native-position"] = position
if (selected) props["data-native-selected"] = ""
return createElement("div", props, children)
}
No state or effects, just a function that turns props into data
attributes. The less framework-specific logic lives in these
components, the less can go wrong.
I don't use Inertia day-to-day, and it shows!
I'm a Rails and ERB developer. I don't reach for React or Vue on my
own projects. When I built the Inertia support, I leaned heavily on
early Ruby Native adopters who use Inertia every day. They told me
when something felt off, when a prop name was confusing, or when a
pattern didn't match how they structure their apps.
This is something I think library authors underestimate. You can read
the docs and follow the conventions, but there's a feel to a
framework that only comes from daily use. Having people who live in
that framework test your API is the difference between "technically
works" and "feels right."
Three frameworks from one test bundle
Supporting multiple frameworks creates a real regression problem. A
change to the JavaScript bridge could break React but not ERB. A new
signal element might work in Hotwire but lead to a race condition in
Inertia. And I don't want to manually check every combination on
every change.
So I set up XCUITest tests for each of the three demo apps: Beervana
(Hotwire/ERB), Coffee (React/Inertia), and Habits (Vue/Inertia). Each
test suite boots the real Rails server and exercises the actual
native UI.
The tests don't assert on HTML or JavaScript. They assert on what the
user sees:
* Does the tab bar appear after sign-in?
* Does the navbar title update when I switch tabs?
* Does the menu button open with the right items?
Here's what the Coffee (React) test looks like for the navbar menu:
// Menu button: Account page has an ellipsis button with menu items.
let menuButton = app.navigationBars.buttons["More"]
XCTAssertTrue(menuButton.waitForExistence(timeout: 3))
menuButton.tap()
let editAction = app.buttons["Edit profile"]
XCTAssertTrue(editAction.waitForExistence(timeout: 3))
And the equivalent Beervana (Hotwire) test for tab navigation:
app.tabBars.buttons["Passport"].tap()
XCTAssertTrue(app.navigationBars["Passport"].waitForExistence(timeout: 5))
app.tabBars.buttons["Profile"].tap()
XCTAssertTrue(app.navigationBars["Profile"].waitForExistence(timeout: 5))
Both tests are framework-agnostic. They have no idea what's rendering
the HTML. They just verify the native UI works. If I break something
in the JavaScript bridge, these tests catch it regardless of which
framework exposed the problem.
And then someone asked about Sinatra...
I haven't tried it yet, but someone recently asked if Ruby Native
works outside of Rails. With Sinatra, for example. And the honest
answer is: it should. The signal elements are just HTML. The native
app reads the DOM. There's nothing Rails-specific about the data
attributes themselves.
The Ruby gem's helpers are Rails-specific, sure. But the React and
Vue components aren't. And you could render the raw HTML from any
templating language.
That question made me realize the early decision to build on data
attributes rather than framework hooks is paying off in ways I didn't
plan for. It's one more framework to think about. But the fact that
it's even plausible tells me the abstraction is at the right layer.
---------------------------------------------------------------------
If you're using Inertia with Rails and want to try Ruby Native, I'd
love your feedback. The people who use these frameworks daily are the
ones who make the API better.
Message Joe Masilotti
7
1
Share
Discussion about this post
CommentsRestacks
User's avatar
[ ]
[ ]
[ ]
[ ]
TopLatestDiscussions
No posts
Ready for more?
[ ]
Subscribe
(c) 2026 Joe Masilotti * Privacy [?] Terms [?] Collection notice
Start your SubstackGet the app
Substack is the home for great culture
This site requires JavaScript to run correctly. Please turn on
JavaScript or unblock scripts