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

Introducing ReActionView: A new ActionView-Compatible ERB Engine @ Kaigi on Rails 2025, Tokyo, Japan

After introducing Herb, the HTML-aware ERB parser at RubyKaigi in April, I’m now releasing ReActionView - a drop-in ActionView replacement bringing LiveView-style reactivity to Rails while maintaining HTML-over-the-wire philosophy.

This talk completes a trilogy by delivering the practical payoff: the ActionView-compatible ReActionView engine, built on Herb, the new Prism-powered HTML-aware ERB parser.

ReActionView maintains ActionView compatibility while bringing HTML-awareness and reactivity to HTML+ERB views.

https://kaigionrails.org/2025/talks/marcoroth

Avatar for Marco Roth

Marco Roth

September 27, 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. 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.
  16. ERB

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

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

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

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

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

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

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

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

    ", :text], ["name", :expr], ["!", :text], ["</", :text], ["h1", :text], [">", :text], ]
  30. _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
  31. # 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
  32. # 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
  33. # 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
  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" autoload :Herb, "action_view/template/handlers/erb/herb" 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: Herb end end end end
  37. $

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

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

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

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

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

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

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

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

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

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