Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introducing ReActionView: A new ActionView-comp...

Avatar for Marco Roth Marco Roth
September 05, 2025

Introducing ReActionView: A new ActionView-compatible ERB Engine @ Rails World 2025, Amsterdam

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.

At Rails World, I’ll debut ReActionView: a new ERB engine built on Herb, fully compatible with .html.erb but with HTML validation, better error feedback, reactive updates, and built-in tooling.
This will be the first public release, exclusive to Rails World, tying together everything from the past talks and even bringing exclusive updates to tools I first showed at Rails World 2023.

https://rubyonrails.org/world/2025/day-2/marco-roth

Avatar for Marco Roth

Marco Roth

September 05, 2025
Tweet

More Decks by Marco Roth

Other Decks in Programming

Transcript

  1. Marco Roth !  @marcoroth_  @[email protected] 🌐 marcoroth.dev 

    @marcoroth Full-Stack Developer & Open Source Contributor  @marcoroth.dev
  2. <%= tag.div data: { controller: "hello" } do %> <%=

    content_tag :input, data: { hello_target: "name"} %> <%= button_tag data: { action: "click->hello#greet" } do %> Greet <% end %> <%= tag.span data: { hello_target: "output" } %> <% end %>
  3. I had a few use-cases for a smart ERB parser

    that could handle use-cases like this
  4. HTML-aware ERB Parser HTML-aware ERB parser Written in C99 Built

    on Prism Designed for tooling Support for Action View Helpers
  5. <% if valid? %> <h1>Title</h1> <% end %> <% if

    valid? %> <h1>Title</h1> <% end %>
  6. <% if valid? %> <h1>Title</h1> <% end %> @ ERBIfNode

    ├── tag_opening: "<%" ├── content: " if valid? " ├── tag_closing: "%>" ├── statements: │ └── @ HTMLElementNode │ ├── open_tag: │ │ └── @ HTMLOpenTagNode │ │ ├── tag_opening: "<" │ │ ├── tag_name: "h1" │ │ ├── tag_closing: ">" │ │ ├── children: [] │ │ └── is_void: false │ │ │ ├── tag_name: "h1" │ ├── body: │ │ └── @ HTMLTextNode │ │ └── content: "Title" │ │ │ └── close_tag: │ └── @ HTMLCloseTagNode │ ├── tag_opening: "</" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── subsequent: ∅ └── end_node: └── @ ERBEndNode ├── tag_opening: "<%" ├── content: " end " └── tag_closing: "%>"
  7. <% if valid? %> <h1>Title</h1> <% end %> @ ERBIfNode

    ├── tag_opening: "<%" ├── content: " if valid? " ├── tag_closing: "%>" ├── statements: │ └── @ HTMLElementNode │ ├── open_tag: │ │ └── @ HTMLOpenTagNode │ │ ├── tag_opening: "<" │ │ ├── tag_name: "h1" │ │ ├── tag_closing: ">" │ │ ├── children: [] │ │ └── is_void: false │ │ │ ├── tag_name: "h1" │ ├── body: │ │ └── @ HTMLTextNode │ │ └── content: "Title" │ │ │ └── close_tag: │ └── @ HTMLCloseTagNode │ ├── tag_opening: "</" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── subsequent: ∅ └── end_node: └── @ ERBEndNode ├── tag_opening: "<%" ├── content: " end " └── tag_closing: "%>"
  8. <% if valid? %> <h1>Title</h1> <% end %> @ ERBIfNode

    ├── tag_opening: "<%" ├── content: " if valid? " ├── tag_closing: "%>" ├── statements: │ └── @ HTMLElementNode │ ├── open_tag: │ │ └── @ HTMLOpenTagNode │ │ ├── tag_opening: "<" │ │ ├── tag_name: "h1" │ │ ├── tag_closing: ">" │ │ ├── children: [] │ │ └── is_void: false │ │ │ ├── tag_name: "h1" │ ├── body: │ │ └── @ HTMLTextNode │ │ └── content: "Title" │ │ │ └── close_tag: │ └── @ HTMLCloseTagNode │ ├── tag_opening: "</" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── subsequent: ∅ └── end_node: └── @ ERBEndNode ├── tag_opening: "<%" ├── content: " end " └── tag_closing: "%>"
  9. <% if valid? %> <h1>Title</h1> <% end %> @ ERBIfNode

    ├── tag_opening: "<%" ├── content: " if valid? " ├── tag_closing: "%>" ├── statements: │ └── @ HTMLElementNode │ ├── open_tag: │ │ └── @ HTMLOpenTagNode │ │ ├── tag_opening: "<" │ │ ├── tag_name: "h1" │ │ ├── tag_closing: ">" │ │ ├── children: [] │ │ └── is_void: false │ │ │ ├── tag_name: "h1" │ ├── body: │ │ └── @ HTMLTextNode │ │ └── content: "Title" │ │ │ └── close_tag: │ └── @ HTMLCloseTagNode │ ├── tag_opening: "</" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── subsequent: ∅ └── end_node: └── @ ERBEndNode ├── tag_opening: "<%" ├── content: " end " └── tag_closing: "%>"
  10. <% if valid? %> <h1>Title</h1> <% end %> @ ERBIfNode

    ├── tag_opening: "<%" ├── content: " if valid? " ├── tag_closing: "%>" ├── statements: │ └── @ HTMLElementNode │ ├── open_tag: │ │ └── @ HTMLOpenTagNode │ │ ├── tag_opening: "<" │ │ ├── tag_name: "h1" │ │ ├── tag_closing: ">" │ │ ├── children: [] │ │ └── is_void: false │ │ │ ├── tag_name: "h1" │ ├── body: │ │ └── @ HTMLTextNode │ │ └── content: "Title" │ │ │ └── close_tag: │ └── @ HTMLCloseTagNode │ ├── tag_opening: "</" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── subsequent: ∅ └── end_node: └── @ ERBEndNode ├── tag_opening: "<%" ├── content: " end " └── tag_closing: "%>"
  11. <% if valid? %> <h1>Title</h1> <% end %> @ ERBIfNode

    ├── tag_opening: "<%" ├── content: " if valid? " ├── tag_closing: "%>" ├── statements: │ └── @ HTMLElementNode │ ├── open_tag: │ │ └── @ HTMLOpenTagNode │ │ ├── tag_opening: "<" │ │ ├── tag_name: "h1" │ │ ├── tag_closing: ">" │ │ ├── children: [] │ │ └── is_void: false │ │ │ ├── tag_name: "h1" │ ├── body: │ │ └── @ HTMLTextNode │ │ └── content: "Title" │ │ │ └── close_tag: │ └── @ HTMLCloseTagNode │ ├── tag_opening: "</" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── subsequent: ∅ └── end_node: └── @ ERBEndNode ├── tag_opening: "<%" ├── content: " end " └── tag_closing: "%>"
  12. <div> <h1><%= @title %></h1> <% if user_signed_in? %> <p>Welcome, <%=

    current_user.name %>!</p> <% else %> <p>Please <%= link_to "sign in", login_path %></p> <% end %> </div>
  13. template = ActionView::Template.new( "<h1>Hello, <%= name %>!", "test.html.erb", ActionView::Template::Handlers::ERB, locals:

    [:name] ) compiled = template.handler.call( template, template.source ) <h1>Hello, <%= name %>!
  14. compiled = template.handler.call( template, template.source ) $ Tag `<h1>` opened

    at (1:1) was never closed before the end of document. UnclosedElementError in users/index.html.herb
  15. September 5, 2025 - Rails World 2025, Amsterdam Marco Roth

    Introducing ReActionView: A new ActionView-Compatible ERB Engine
  16. Rails is a open source web-application framework for Ruby. It

    ships with an answer for every letter in MVC: Action Pack for the Controller and View, Active Record for the Model.
  17. ERB

  18. "So basically the templates are plain text files that can

    hold anything (HTML, XML, LaTeX, emails) sprinkled with Ruby embeddings to add dynamic behavior."
  19. <dialog> Web Components Progressive Web Apps (PWA) Custom Elements <script

    type="importmap"> inert modulepreload Declarative Shadow DOM
  20. SSR Rails App + HTML-over-the-wire Hotwire/HTMX/Unpoly Rails API + Single

    Page App React/Vue/Svelte/etc. CSR server-side rendering client-side rendering
  21. Let's try to close the gap, so less and less

    teams actually need actually to abandon Action View
  22. _buf = ::String.new _buf << '<h1>Hello, ' _buf << ::Erubi.h(name)

    _buf << '!</h1>' _buf.to_s (simplified+formatted)
  23. @ DocumentNode └── children: (1 item) └── @ HTMLElementNode ├──

    open_tag: │ └── @ HTMLOpenTagNode │ ├── tag_opening: "<" │ ├── tag_name: "h1" │ └── tag_closing: ">" │ ├── tag_name: "h1" ├── body: (3 items) │ ├── @ HTMLTextNode │ │ └── content: "Hello, " │ │ │ ├── @ ERBContentNode │ │ ├── tag_opening: "<%=" │ │ ├── content: " name " │ │ └── tag_closing: "%>" │ │ │ └── @ HTMLTextNode │ └── content: "!" │ └── close_tag: └── @ HTMLCloseTagNode ├── tag_opening: "</" ├── tag_name: "h1" └── tag_closing: ">"
  24. @ DocumentNode └── children: (1 item) └── @ HTMLElementNode ├──

    open_tag: │ └── @ HTMLOpenTagNode (<h1>) │ ├── body: (3 items) │ ├── @ HTMLTextNode ("Hello, ") │ ├── @ ERBContentNode (<%= name %>) │ └── @ HTMLTextNode ("!") │ └── close_tag: └── @ HTMLCloseTagNode (</h1>)
  25. class Compiler < Herb::Visitor def visit_document_node(node) def visit_html_element_node(node) def visit_html_open_tag_node(node)

    def visit_html_close_tag_node(node) def visit_html_text_node(node) def visit_erb_content_node(node) end
  26. def visit_html_element_node(node) visit(node.open_tag) visit_all(node.body) visit(node.close_tag) end @ HTMLElementNode ├── open_tag:

    │ └── @ HTMLOpenTagNode │ ├── body: (3 items) │ ├── @ HTMLTextNode │ ├── @ ERBContentNode │ └── @ HTMLTextNode │ └── close_tag: └── @ HTMLCloseTagNode
  27. @ ERBContentNode ├── tag_opening: "<%=" ├── content: " name "

    └── tag_closing: "%>" def visit_erb_content_node(node) if node.tag_opening == "<%=" add_expression(node.content) else add_code(node.content) end end
  28. def add_text(text) @tokens << [:text, text] end def add_expression(code) @tokens

    << [:expr, code] end def add_code(code) @tokens << [:code, code] end
  29. @tokens = [ ["<", :text], ["h1", :text], [">", :text], ["Hello,

    ", :text], ["name", :expr], ["!", :text], ["</", :text], ["h1", :text], [">", :text], ]
  30. @tokens = [ ["<", :text], ["h1", :text], [">", :text], ["Hello,

    ", :text], ["name", :expr], ["!", :text], ["</", :text], ["h1", :text], [">", :text], ]
  31. _buf = ::String.new _buf << '<h1>Hello, ' _buf << ::Erubi.h(name)

    _buf << '!</h1>' _buf.to_s (simplified+formatted) _buf = ::String.new _buf << '<h1>Hello, ' _buf << ::Herb.h(name) _buf << '!</h1>' _buf.to_s Erubi::Engine Herb::Engine
  32. # actionview/lib/action_view/template/handlers/erb/erubi.rb require "erubi" module ActionView class Template module Handlers

    class ERB class Erubi < ::Erubi::Engine def initialize(input, properties = {}) # ... super end end end end end end
  33. # actionview/lib/action_view/template/handlers/erb/herb.rb require "herb" module ActionView class Template module Handlers

    class ERB class Herb < ::Herb::Engine def initialize(input, properties = {}) # ... super end end end end end end
  34. # actionview/lib/action_view/template/handlers/erb.rb module ActionView class Template module Handlers class ERB

    autoload :Erubi, "action_view/template/handlers/erb/erubi" class_attribute :erb_implementation, default: Erubi end end end end
  35. # actionview/lib/action_view/template/handlers/erb.rb module ActionView class Template module Handlers class ERB

    autoload :Erubi, "action_view/template/handlers/erb/erubi" autoload :Herb, "action_view/template/handlers/erb/herb" class_attribute :erb_implementation, default: Erubi end end end end
  36. # actionview/lib/action_view/template/handlers/erb.rb module ActionView class Template module Handlers class ERB

    autoload :Erubi, "action_view/template/handlers/erb/erubi" autoload :Herb, "action_view/template/handlers/erb/herb" class_attribute :erb_implementation, default: Erubi end end end end
  37. # actionview/lib/action_view/template/handlers/erb.rb module ActionView class Template module Handlers class ERB

    autoload :Erubi, "action_view/template/handlers/erb/erubi" autoload :Herb, "action_view/template/handlers/erb/herb" class_attribute :erb_implementation, default: Herb end end end end
  38. %

  39. But since we deal with HTML+ERB we can also have

    some smart validations built-in
  40. Level 1 - Better Errors Messages Level 2 - HTML-aware

    Engine Level 3 - Action View Optimizations Level 4 - Reactive ERB Views Level 5 - Client-side Templates Level 6 - External Components
  41. # config/initializers/reactionview.rb ReActionView.configure do |config| # Intercept .html.erb templates #

    config.intercept_erb = true # Enable debug mode in development config.debug_mode = Rails.env.development? end
  42. # config/initializers/reactionview.rb ReActionView.configure do |config| # Intercept .html.erb templates config.intercept_erb

    = true # Enable debug mode in development config.debug_mode = Rails.env.development? end
  43. Please give it a shot, even if it's just locally

    in your development environment
  44. Level 1 - Better Errors Level 2 - HTML-aware Engine

    Level 3 - Action View Optimizations Level 4 - Reactive ERB Views Level 5 - Client-side Templates Level 6 - External Components
  45. @items = [1, 2, 3, 4] <ul> <li>1</li> <li>2</li> <li>3</li>

    <li>4</li> </ul> State View Template Rendered View Log Initial view rendered @items state changed re-rendering view tracing dependencies render delta for item `4` apply delta to view <ul> <% @items.each do |item| %> <li><%= item %></li> <% end %> </ul>
  46. <style scoped> .example { color: red; } </style> <template> <div

    class="example"> Hello World </div> </template>
  47. <style scoped> .example { color: red; } </style> <template> <div

    class="example"> Hello World </div> </template>
  48. <style scoped> color: red; } </style> <template> Hello World </div>

    </template> <div class="example"> .example {
  49. <style> color: red; } </style> <template> Hello World </div> </template>

    <div class="example" data-v-f3f3eg9> .example[data-v-f3f3eg9]
  50. <div class="card"> <h2><%= @product.name %></h2> <turbo-frame id="reviews"> <h3>Customer Reviews</h3> <%=

    render @reviews %> <%= link_to "Load More Reviews", reviews_path %> </turbo-frame> <!-- ... --> </div>
  51. <div class="card"> <h2><%= @product.name %></h2> <turbo-frame id="reviews"> <h3>Customer Reviews</h3> <%=

    render @reviews %> <%= link_to "Load More Reviews", reviews_path %> </turbo-frame> <!-- ... --> </div>
  52. <div class="card"> <h2><%= @product.name %></h2> <turbo-frame id="reviews"> <h3>Customer Reviews</h3> <%=

    render @reviews %> <%= link_to "Load More Reviews", reviews_path %> </turbo-frame> <!-- ... --> </div>
  53. If we, as a framework and community, want to stay

    relevant we need to explore what's possible.
  54. ReActionView started as a vision to address some of the

    shortcomings in the Rails View Layer
  55. Thank you &  @marcoroth_  @[email protected] 🌐 marcoroth.dev 

    @marcoroth  @marcoroth.dev  /in/marco-roth