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 18, 2025

Introducing ReActionView: A new ActionView-compatible ERB Engine @ EuRuKo 2025, Viana do Castelo, Portugal

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.

Now, I’ll continue with 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.

https://app.euruko.org/sessions/introducing-reactionview-a-new-actionview-compatible-erb-engine

Avatar for Marco Roth

Marco Roth

September 18, 2025
Tweet

More Decks by Marco Roth

Other Decks in Programming

Transcript

  1. September 18, 2025 - EuRuKo 2025, Viana do Castelo, Portugal

    Marco Roth Introducing ReActionView: A new ActionView-Compatible ERB Engine
  2. Marco Roth !  @marcoroth_  @[email protected] 🌐 marcoroth.dev 

    @marcoroth Full-Stack Developer & Open Source Contributor  @marcoroth.dev
  3. <%= 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 %>
  4. I had a few use-cases for a smart ERB parser

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

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

    valid? %> <h1>Title</h1> <% end %>
  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. <% 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: "%>"
  13. <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>
  14. 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 %>!
  15. 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
  16. September 18, 2025 - EuRuKo 2025, Viana do Castelo, Portugal

    Marco Roth Introducing ReActionView: A new ActionView-Compatible ERB Engine
  17. 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.
  18. ERB

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

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

    type="importmap"> inert modulepreload Declarative Shadow DOM
  21. 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
  22. Let's try to close the gap, so less and less

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

    _buf << '!</h1>' _buf.to_s (simplified+formatted)
  24. @ 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: ">"
  25. @ DocumentNode └── children: (1 item) └── @ HTMLElementNode ├──

    open_tag: │ └── @ HTMLOpenTagNode (<h1>) │ ├── body: (3 items) │ ├── @ HTMLTextNode ("Hello, ") │ ├── @ ERBContentNode (<%= name %>) │ └── @ HTMLTextNode ("!") │ └── close_tag: └── @ HTMLCloseTagNode (</h1>)
  26. 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
  27. 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
  28. @ 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
  29. def add_text(text) @tokens << [:text, text] end def add_expression(code) @tokens

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

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

    ", :text], ["name", :expr], ["!", :text], ["</", :text], ["h1", :text], [">", :text], ]
  32. _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
  33. # 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
  34. # 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
  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" 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: Erubi end end end end
  38. # 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
  39. $

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

    some smart validations built-in
  41. 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
  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. # 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
  44. Please give it a shot, even if it's just locally

    in your development environment
  45. 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
  46. @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>
  47. <style scoped> .example { color: red; } </style> <template> <div

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

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

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

    <div class="example" data-v-f3f3eg9> .example[data-v-f3f3eg9]
  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. <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>
  54. If we, as a framework and community, want to stay

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

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

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