Logo marcoroth.dev

Rails World 2025 Recap Introducing ReActionView: A new ActionView-Compatible ERB Engine and initiative for the Rails view layer

Marco Roth

24 min read

Rails World 2025 just wrapped up in Amsterdam last week. This blog post is a recap of my talk titled “Introducing ReActionView: A new ActionView-Compatible ERB Engine” and summarizes my talk, the new releases, and where my view-layer work is headed next.

This talk is the conclusion of a journey I’ve been sharing throughout 2025. At RubyKaigi, I introduced Herb: a new HTML-aware ERB parser and tooling ecosystem. At RailsConf, I released developer tools built on Herb, including a formatter, linter, and language server, alongside a vision for modernizing and improving the Rails view layer.

RubyKaigi, RailsConf and Rails World 2025

At Rails World, I started to deliver on the vision I shared at RailsConf. I publicly introduced ReActionView, an initiative to explore what’s possible in the Rails view layer, plus a significant update to Herb with v0.7 and Herb::Engine.

Herb::Engine is a new ERB engine built on the Herb Parser, fully compatible with .html.erb but with HTML validation, better error feedback, reactive updates (soon), and built-in tooling.

This is tying together everything from the past talks and the work I have done so far in 2025 to show the full potential of Herb, ReActionView, and where the Rails view layer could be headed.

Slides: Speaker Deck
Projects: ReActionViewHerbStimulus Lint
Recording: (coming soon)


What we announced

Rails World 2025 summary slide: ReActionView, Herb::Engine, Dev Tools, Linters, and more

  • ReActionView initiative: a pragmatic, ActionView-compatible path forward for the view layer.
  • Herb v0.7: updates across the Linter, Formatter, Language Server, and VS Code extension.
  • Herb::Engine: an HTML-aware, framework-independent ERB rendering engine.
  • Dev Tools for the browser: view/partial/component outlines with jump-to-source, ERB output hover, and dismissable validation overlays during development.
  • New linter rules: +18 rules (9 accessibility focused), bringing the total to 32.
  • Stimulus Lint The extracted diagnostics from Stimulus LSP (powered by Herb).
  • Tailwind class rewriter (coming soon).
  • Early experiments toward Phoenix LiveView-style reactivity for ERB.

Herb Linter updates since RailsConf

  • 18 new rules (including 9 accessibility), for a total 32 linter rules.
  • CLI supports --github flag for annotating pull requests.
  • More advanced control flow analysis
Herb Linter Control Flow Analysis

Herb Formatter updates since RailsConf

The Herb Formatter got a lot of improvements in terms of stability and predictability. We are getting really close to enabling the formatter by default in the Herb Language Server, so it formats HTML+ERB files on save.

Another thing we have been working on is a rewriter system for the formatter, so we can rewrite part of the document before and/or after formatting.

One of the most requested features is to be able to format the Tailwind CSS classes inside the class attribute. Tailwind has an official recommended way for ordering classes but it’s tied to the Prettier ecosystem and doesn’t really work well with HTML+ERB documents.

So we extracted the algorithm into a new package @herb-tools/tailwind-class-sorter that the algorithm can be used independently of the prettier plugin.

The Herb Formatter is going to have an option to integrate this package so we automatically sort the Tailwind classes when saving the document.

Since the Herb Parser understands both HTML, ERB, and Ruby it’s also going to work when you use Action View Tag Helpers like tag.div, content_tag, link_to, and more.

Stimulus Lint

The Stimulus LSP has had a lot of diagnostics that help you catch errors when working with Stimulus. But these diagnostics where exclusive and locked into the Language Server implementation, so you couldn’t use them independently.

We now started to work on extracting these diagnostics into a new Stimulus Lint package, which is powered by the Herb Linter infrastructure, so it’s going to work on both HTML and HTML+ERB documents.

Stimulus Lint uses the Stimulus Parser project to statically analyze your project to figure out which controllers, targets, values, classes, etc. are available, so it can tell it when you are using undefined or unknown controllers.

The other benefit is that you can run the linter independently from the language server, which makes it ideal to run in CI or locally in a pre-commit hook.

Since we are building on top of the Herb Linter architecture we also get the nice CLI and beautiful error pinpointing with syntax highlighting right in the terminal.

Turbo Lint

We now have the power to better understand HTML+ERB and the Ruby inside the ERB tags, which now also allows us to build more useful tooling and a linter for Turbo.

We are going to do some research to see how useful this really is. If it makes sense, we are also going to publish a Turbo Lint package, specifically tailored for how you use Turbo in HTML+ERB files.

Stimulus LSP Update

With Herb being available now and having the Stimulus Lint extracted as it’s own package, we are going to ship an update the to Stimulus LSP to integrate these advancements.

This will also significantly simplify the Stimulus Language Server implementation, since most of the logic is going to live in Herb and Stimulus Lint.

Since we are using the Herb Parser, and the Herb Parser is able to understand Action View Tag Helpers, we are also going to get these diagnostics on Action View Tag Helpers which are very common in Rails applications.

With the power of Herb, we could also introduce more advanced diagnostics, like find controller/target usages, find unused controllers, refactoring tools (like renaming), and more!

The Rails View Layer

ActionView was introduced at the very start of Rails in July 2004, as part of the Rails 0.5.0 release. Action View was thus the “V” in Rails’ MVC, responsible for template rendering. Back then, it was packaged within Action Pack, not as a separate gem. Even back then, it shipped with ERB (Embedded Ruby), which is still what it’s using today.

At RailsConf, I shared my vision for the Rails view layer using multiple adoption levels. Action View uses Erubi by default today, which is an ERB implementation that operates as a String Template Engine. We want to take it from being a String Template Engine to be a HTML Templating Engine. An engine that cannot produce invalid HTML and an engine that can also tell you what’s wrong about the syntax.

The idea is, if you give the template engine invalid HTML markup or invalid ERB syntax, it shouldn’t compile. And if you still do, it should tell you a) what is wrong, and b) where, within your template, the error is. We want to get more actionable feedback.

So we want to see, how we can reimagine ActionView for 2025 and beyond. We want to see how we can reengineer some of the parts of ActionView to make them more performant, easier to use, and make them more maintainable. And, we want to see if we can make ActionView reactive, so that you even need less JavaScript to build more ambitious web UIs.

So this is the initiative of ReActionView. An initiative to explore what’s possible in the Rails view layer in 2025 and beyond.

The design goals are to keep .html.erb templates backwards-compatible (as long as you already have valid HTML markup), improve the developer experience, embrace modern web standards and new native browser features.

See if we can more tightly integrate modern Frontend Frameworks, and finally, to see how far we can push things in general and what we could achieve with a new wave of ideas, tooling, and libraries.

ReActionView Design Goals

Over the last few years browsers really got a lot of new great features. In HTML+CSS we got Import Maps, Web Components/Custom Elements, Declarative Shadow DOM, Progressive Web Apps (PWA), and so much more.

Advancements in HTML and JavaScript

In CSS, we got CSS Grids, CSS Nesting, Container Queries, CSS @function’s, CSS Custom @property, @layer, and a lot more.

Advancements in CSS

We also got a wave of new Web APIs, including the View Transitions API, Popover and Invoker Commands API, and new upcoming Speculation Rules API, which could be very interesting for the upcoming Turbo offline mode.

Advancements in Web APIs

Comparing traditional Rails apps, like server-side rendering (SRR) using one of the popular HTML-over-wire frameworks, it’s quite far away from the client-side rendering (CSR) approach where you use a Rails API and one of the popular client-side frameworks to build a full Single Page Application (SPA).

Server-side rendering vs. Client-side rendering

But sadly, more and more teams are opting to use the latter because they find that HTML-over-the-wire and it’s frameworks are limiting. Teams want to build High Fidelity UIs, want to use existing Components/Libraries, might already have a lot of React Developers on staff, or have split Backend- and Frontend-Teams.

Hotwire is great, simple, and gets the job done in most of cases. But when you need more than what Hotwire provides you today, it’s not as easy to transition from Hotwire to a Frontend Framework. The process is not gradual. There is no onramp or migration path. You either migrate all or nothing.

And this is part of the reason why some teams opt to use SPAs from the beginning. Which, in my opinion, is giving away most of what makes Rails so great and productive. Let’s try to close the gap, so less and less teams actually need actually to abandon Action View. I want to see what’s possible and how far we can go. That’s why I’ve been investing in Herb to help see what we can do.

Herb v0.7.0 and Herb::Engine

I’m happy to announce the release of Herb v0.7.0.

Herb v0.7.0 Release

Herb v0.7.0 also ships with a first version of the Herb::Engine. The Herb::Engine is a new, HTML-aware ERB Rendering Engine, that’s delivering on the vision I shared at RailsConf.

Herb::Engine - A new HTML-aware ERB Rendering Engine

Herb::Engine is designed to be Erubi::Engine API-compatible and is meant to be a drop-in replacement for when dealing with .html.erb files.

How Erubi::Engine works

As I shared earlier, Erubi::Engine could be categorized as a String Template Engine. Which means that it doesn’t really care what code is around the <% and %> tags. Erubi::Engine uses a Regex to take your template and split it up into static parts and Ruby parts when it’s trying to compile the ERB template.

Erubi: Splitting up static and dynamic parts in the template

After splitting up the template into the individual pieces, it’s calling add_text(), add_expression(), and add_code() to produce the compiled template.

The add_text() is being used for appending the static text parts to the compiled template, add_expression() for appending the String-result of the Ruby code, and add_code() for any other Ruby code that might be used for control flow or other logic.

In our simple example here, we get the following output.

Erubi compiled Template (simplified and formatted)

We are using a String as a buffer to shuffle content into the buffer. At the very end we call .to_s to make sure we return the evaluated and rendered template as a string.

Herb::Engine compared to Erubi::Engine

In contrast, the Herb::Engine is using the Herb Parser to first parse the template.

In our simple example, this produces the following Syntax Tree. If we now strategically visit each of these nodes in the syntax tree we can try to compile the template by visiting all nodes.

Herb ParseResult of the example HTML+ERB template

Since we are using Herb, we can leverage the Herb::Visitor which allows us to implement visit_* methods to strategically visit each of the node in our syntax tree.

Let’s start by implementing the HTMLOpenTagNode. The HTMLOpenTagNode has a tag_opening, tag_name, and tag_closing properties. If we call add_text() for each of these parts we can add them to the static output of our compiled template.

The HTMLTextNode is quite straightforward too. Since it only contains static text, we can also call add_text() for the content.

The ERBContentNode is a bit more complex, since it represents any ERB tag, like <% or <%=. It can check the tag_opening if it is a <%=, and if it is, call add_expression() and otherwise call add_code().

This will call the following methods in the following order as part of visiting each node in the syntax tree.

If we now compare this to Erubi::Engine you can see that this uses more method calls.

But since we are controlling the engine implementation we can slightly change the implementation of add_text, add_expression, and add_code to collect tokens, instead of directly outputting them as part of final compiled template.

After visiting each node in the tree, we can look at what we generated and try to compact the tokens. So if we see multiple consecutive text tokens we rewrite and combine them into a single text token.

If we now call add_text, add_expression, and add_code for each of the tokens in the array, we get the same method calls Erubi::Engine produced.

And if we compare the compiled templates you can also see that the output are identical.

Erubi::Engine and Herb::Engine compiled output compared

The difference is, that Herb::Engine gives as all the guarantees the Herb Parser gives us. Which means we get helpful error messages and exact locations for where something went wrong in our template.

Use Herb::Engine in Rails

Now, let’s try to use our new ERB rendering engine in Rails. Luckily for us, ActionView::Template and ActionView::Template::Handlers makes this very easy for us to plug in our new rendering engine.

We can implement a new Herb ERB implementation within ActionView::Template::Handlers::ERB. Since we designed Herb::Engine to be API-compatible with Erubi::Engine we can pretty much copy the Erubi ERB implementation within Action View.

Within the ActionView::Template::Handlers::ERB handler, we can autoload the Herb implementation and default the erb_implemenation to use our new Herb implementation.

We can now point our app to use our Rails fork and boot our app using bin/dev.

And tada, it renders our application using the Herb::Engine implementation!

Improved Error Pages/Overlays

Since we now have the Herb::Engine implementation and can start to improve other things as well. Let’s start with the error page when we see a syntax error in an HTML+ERB template.

Currently Rails renders an error page which includes the whole template in the exception message, unformatted.

We can improve it, because the Herb Parser already exactly knows where something is wrong in the template. We already make use of this in the Herb Linter in the terminal output:

So let’s bring this detail to the error page in the browser, as well:

This makes it so much more actionable and easier to understand what’s wrong in your template. And the beautiful thing is that you get immediate and accurate feedback in your editor, since the same parser is now responsible for powering the editor tooling and rendering the ERB in your app.

Dismissable Validation Overlays

Since we are already dealing with HTML, we can also try to add validations and report these issues. In this case we can report if you have invalid HTML, in this case nested <p> tags.

We can also report if the template is trying to unsafely interpolate ERB:

Since these issues are not strictly “breaking” the template rendering, we report these violations as issue and make them dismissable.

Debug View Annotations in Development

Having control over the rendering engine means we can also do more cool things. I’ve been really enjoying the config.annotate_rendered_view_with_filenames setting in Rails to annotate the view file names as HTML comments.

The issue with HTML comments is that you can’t really visually style them on the rendered page in the browser.

But, our compiler already knows how to render HTML elements and attributes.

So, what if we inject additional HTML attributes, directly on the HTML elements themselves, instead of adding HTML comments with the filenames in development?

If we render these additional debug Data Attributes and add some CSS styles:

We get really nice visual view outlines, right in the browser!

If we also add another attribute for the “view type”, we can give the different types of view files different colors. So we can say views are blue, partials are green, and components are orange:

If we also add, the relative project path and the full path to the attributes:

We can reveal the full project path when hovering the outline labels:

Another thing we can do, since we have the full path, is the open the file in the editor when clicking the outline label:

Since we are using CSS outline and HTML attributes we don’t interrupt the natural flow of the content on the page.

So the outlines just add the visual appearance and provide immensely helpful visual debugging. And the jump-to-source when clicking on the outline label is super helpful when you are trying to find the right file to update.

Debug ERB Output Tags in Development

The next logical question is: “Can we also do this for ERB Output tags?”. And the obvious answer is, YES!

The Herb Debug menu has a “Show ERB Output Outlines” option.

When enabled, it shows you all the dynamic parts on the current page you are on, which were rendered using a <%= tag, which is super cool!

We also have an option for enabling “Reveal ERB Output tag on hover”.

When enabled, it shows the exact ERB code that was used to produce the content that is rendered on the page.

And the logical next step is to also implement the jump-to-source when clicking on the ERB output tags. In this case, it will open the right view file, on the right line and the right column, since the Herb Parser exactly knows where these tags were located in the original template file. Super useful!

Roadmap: ReActionView Adoption Levels

So, what’s next? ReActionView is not a replacement for Action View. It’s a set of ideas and tools that remain Rails-y, aim for backwards-compatibility, and adopt modern web standards. The goal is to raise the floor of DX while keeping the on-ramp low.

In the original vision at RailsConf, I showed 6 Adoption Levels of how far we can take ReActionView. With this implementation we have today we reached and implemented Level 2 of that vision.

Level 1 and 2: Try it locally, today!

You can try these features today in your Rails app. Add reactionview to your Gemfile and run the installer.

bundle add reactionview
 
rails generate reactionview:install

This will generate the following config/initializer/reactionview.rb initializer:

You can choose to intercept all .html.erb files to be rendered and proccessed by Herb::Engine by setting config.intercept_erb = true:

By doing so, you will get the Herb Dev Tools overlay in your Rails app:

Please give it a shot in your app, even if it’s just locally in your development environment and report any unexpected behavior you might encounter.

Level 3 - Action View Optimizations

Since we have so much introspection of these HTML+ERB files, there might be some opportunities to implement more Action View Optimizations, like inlining Partial render calls, inlining Action View Tag Helpers, minifying insignificant whitespace, and probably more.

Level 4 - Reactive ERB Views

With rendering and structural awareness in place, Herb could diff templates and re-render only what changed. In this case, we would consider all instance variables, passed from the controller to the view, as part of the “view state”.

If you update the state, the engine would know which part in the template is going to be affected by this state change, and would be able to only re-render the relevant part of the template, without the need to re-render the whole view.

Reactive ERB templates

Think of this as Phoenix LiveView HEEx-like updates, but still keeping it Rails-y by using the existing .html.erb view files.

If you are not familiar with Phoenix LiveView, you can also think of this as the ERB engine emitting a set of <turbo-stream> update actions to reflect the changes in the DOM, but without the user having to explicitly tell the engine what to render and what to target using an element/ID in the DOM.

This would also allow for a lot of applications to be simplified and could reduce the need for Turbo Frames, Turbo Streams or Turbo Morphing in the future, since the engine itself is fully aware of the state and how it has to reflect the updates in the DOM.

For this to be viable we would need to find a way to serialize the state, detect state changes and then be able to broadcast these updates to the browser.

Level 5 - Client-side templates

Another interesting thing to explore is similar to what React did with React Server Components (RSC). React allows certain React components to be both client-side and server-side rendered, as long as they only use a subset of the full React API.

We could do something similar, where we could bring certain HTML+ERB templates/partials from the server-side to the client-side and allow them to be (re-)rendered on either the server or the client as long as they only use a limited subset of ERB, essentially “ERB client components”.

In that case, templates would be compiled or transpiled for client-side hydration, which could allow for use cases like enabling optimistic UI updates, improved offline support, or limited interactivity without full server round-trips.

Ideally, the partials/components would only take in value objects using primitive types that we would be able to support in both Ruby and JavaScript. Full-blown object like ActiveRecord instances or collections wouldn’t be supported.

But, this is something we could detect at runtime in development and guide users on how to architect their partials/components to be fully compatible so that these templates could be used server- and client-side.

Level 6 - External Components

And finally, we explore the ability to mount external UI components (like React, Vue, Svelte, …) directly within *.html.herb templates. This gives users the flexibility to use the rich ecosystem of already available components in the JavaScript ecosystem without having to abandon the Rails view layer.

The idea is to have a initializer file, similar to how importmap-rails does:

importmap-rails package mapping

In that file (like config/initializers/reactionview.rb) you would be able to register existing components from NPM packages, or tell it to import components from a certain directory within your app:

Register External Components in ReActionView

With the components registered, they will be automatically available within the context of *.html.herb files:

Using React Components in `.herb` files

This approach is less invasive compared to Inertia.js Rails which requires you to render a whole page with Inertia.

With this approach users are still able to use ERB next to their components in the same view template, without having to give up anything.

The engine would know which components are client-side and would know how to render them server-side so that can be mounted/hydrated on the client-side.

This approach is very similar to Turbo Mount, with the difference that this is built right into the engine, is tightly integrated, has built-in editor tooling, requires no setup and just works out of the box.

More Ideas to Explore

One-off Stimulus Controllers

I heared a lot of people are somewhat frustrated with “one-off” Stimulus Controllers in their application. One-off Stimulus Controllers that are only really ever used in one place within the app. A lot of people have been recommending Alpine.js for that use case.

I like a lot of the ideas and advancements Alpine.js brings to the table the over Stimulus, but the idea of writing raw JavaScript inside HTML attributes somehow feels really wrong to me.

There might be some more opportunities to create some kind of “Inline Stimulus” version too. I really like the way Svelte handles this.

Instead of having the Stimulus Controller in it’s own *_controller.js file, we could think of way to have a <script> tag within the file that’s only being transformed/applied to the current file you are in. So you could write some custom one-off JavaScript that’s only being used and available/accessible in this one file you are writing the code in.

View File Scoped CSS

We could do something very similar to CSS and <style> tags as well. This is largely inspired by Vue.js, that allows you to use a special <style scoped> tag, that gets transformed when the template gets compiled.

The idea is that you can write a <style scoped> tag, and all of the CSS rules you are declaring are being scoped to that single file.

In this case, the <style scoped> gets transformed to a regular <style> tag and gets scoped with a special selector that gets inserted at compile time.

In this case, it’s adding a new data-* attribute to both all CSS rules within the <style> tag, and the data attribute to the HTML element itself that’s being used to scope the style. With that, we make sure that none of these styles are leaking to other parts of the application, which is quite clever.

Selective Rendering

And the final idea is “Selective Rendering”. This might come in super handy when paired with concepts like Turbo Frames. Let’s say you have the following view file:

Instead of rendering the whole view, we can pass the partial an argument to look for a certain selector.

Since Herb knows about the structure, hierarchy, the HTML elements and it’s attributes we could filter by a certain selector you pass it, and precompile a subsection of that template.

In the case of the Turbo Frame and the #reviews selector we could prepare and precompile only the following subset of of the bigger template.

This would allow to optimize render performance, since you only ever render what you actually need, which in the case of Turbo Frames is usually only the Turbo Frame itself.

The good thing is that we could also tell that a certain selector wouldn’t match anything in the given template, since we can statically analyze and search for these selectors within the template you are trying to precompile and render.


Why this direction

Action View and ERB are here to stay. Rails remains a full-stack framework built around server-rendered HTML. If we want that story to continue to scale, we need first-class HTML tooling and a clear on-ramp to richer UIs when needed, without forcing apps to abandon ERB.

The idea is to bring more of these modern frontend features to Rails so that there is less of a need to abandon the Rails way. Rails shouldn’t discourage the use of (modern) JavaScript. Rails should embrace it. Rails should also embrace modern web standards and the exciting new APIs and features browsers ship with today.

Rails should provide good defaults and built-in options/migration paths when you need more. This is what Rails was always known for. I believe that the view layer needs to scale in the same way as everything else in the Rails framework does.

But it’s important to me to say that this is a vision. If we, as a framework and community, want to stay relevant we need to explore what’s possible. New innovations require exploration.

Even if none of this is viable in production it’s still worth exploring. The Herb tooling that came out of this is so valuable already, and I also believe that there’s more to achieve with the ReActionView initiative.

Conclusion

Prism had a big effect on Ruby internals and the tooling landscape in general. Prism now ships with Ruby 3.4+ as the default parser. And I believe Herb already had a similar effect for HTML Templating and Tooling.

Herb started as a Parser and is now an ecosystem of valuable tools that Rails developers are using on a daily basis. Meanwhile, ReActionView started as a vision to address some of the shortcomings in the Rails View Layer. Today it might be just an alternative ERB Rendering engine, but I believe that this already shows what could be possible.

We can push more boundaries and bring the whole Rails Framework forward so that it can hold up with the current JavaScript stacks. Also so that teams don’t have to abandon the beauty of Action View.

The community has built incredible tooling for Ruby itself. Now it’s time to bring that same care to the view layer. With Herb and ReActionView, I believe, we have a path to level up the view layer and make Rails’ story awesome for the frontend.

Rails has a unique position as a full stack framework and I want to keep it that way. I want to keep Rails as attractive and competitive as possible for both people that are new and people that have been using Rails for a long time.


I’m incredibly grateful for all the support, feedback, encouragement, and motivation I got over the span of 2025.

Thank you to everyone who came to my talk, shared ideas/feedback, or took the time to chat in Amsterdam. ❤️

If you try the tools and hit issue, please feel free to open an issue. The feedback loop is what makes this really useful.

Rails World 2025 Summary Slide

I’m super excited about the future of the Rails view layer, and would love to see how far we can take it!

Thank you!

— Marco


Sign up for my personal newsletter

Code blocks highlighted by Torchlight.