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

The Modern View Layer Rails Deserves: A Vision ...

The Modern View Layer Rails Deserves: A Vision For 2025 And Beyond @ RailsConf 2025, Philadelphia, PA

## Abstract

Rails revolutionized web development and continues to evolve, but its view layer has remained largely unchanged while frontend needs have evolved dramatically. Rails has maintained its relevance by adopting technologies like Turbolinks and now Turbo/Hotwire while preserving its core principles.

But today's developers face challenges the current view layer wasn't designed to solve: complex UI interactions, reactivity, robust tooling for large codebases, intergration with modern UI kits, and modern tooling expectations.

This talk explores how a new HTML-aware ERB parser (Herb) could enable a truly reactive Rails view layer, bringing LiveView-style reactivity while preserving the "HTML-over-the-wire" philosophy. It will integrate with existing LSPs, unlock powerful tooling, and enable reactive server-rendered templates that could be reused client-side.

I'll demonstrate what's possible through proof-of-concepts and early prototypes, showing how we can collectively advance Rails views for modern development.

## Details

This session shares the vision of "ReActionView - Reactive ActionView" - a modern approach for ActionView built on Herb, designed to meet today's frontend demands while maintaining Rails' principles.

### Content

* **Today's Challenges**: Why Rails views need better tooling in the HTML-over-the-wire era
* **The Foundation**: How Herb opens new possibilities by understanding both HTML and ERB simultaneously
* **Developer Experience**: Seamless integration with Ruby, Rails, Stimulus, and Turbo LSPs
* **Modern Tooling**: Formatter, linter, and intelligent code completion for the view layer
* **Reactive Templates**: Server-side templates with LiveView-inspired reactivity without abandoning .html.erb
* **Universal Templates**: How server templates could potentially work client-side through transpilation

### Highlights

* **Technical Breakthroughs**: How advanced parsing enables next-generation view tooling
* **Prototype Demos**: Early demonstrations of the enhanced developer experience
* **Architecture Options**: Approaches for reactivity while maintaining Rails' server-centric philosophy
* **Community Roadmap**: Steps needed to bring these ideas to a production-ready HTML rendering engine

Avatar for Marco Roth

Marco Roth

July 10, 2025
Tweet

More Decks by Marco Roth

Other Decks in Programming

Transcript

  1. Marco Roth 👋 t @marcoroth_ M @[email protected] g marcoroth.dev g

    @marcoroth Full-Stack Developer & Open Source Contributor b @marcoroth.dev
  2. 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.
  3. ERB

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

    hold anything (HTML, XML, LaTeX, emails) sprinkled with Ruby embeddings to add dynamic behavior."
  5. <h1 > All Posts</h1> <ul> <% @posts.each do |post| %>

    <li> <%= link_to post.title, post_path(post) %> </li> <% end %> </ul>
  6. I think Rails is in a very good spot to

    adopt a coherent and seamless view layer.
  7. <%= 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 %>
  8. HTML-aware ERB Parser HTML-aware ERB parser Written in C99 Built

    on Prism Designed for tooling Support for Action View Helpers
  9. <div> <span><%= @user.firstname %></span> <b><%= @user.lastname %></b> </div> <div> <span><%=

    @user.firstname %></span> <b><%= @user.lastname %></b> </div>
  10. Ruby HTML <div> <span><%= @user.firstname %></span> <b><%= @user.lastname %></b> </div>

    <div> <span><%= @user.firstname %></span> <b><%= @user.lastname %></b> </div>
  11. Ruby HTML <div> <span><%= @user.firstname %></span> <b><%= @user.lastname %></b> </div>

    <div> <span><%= @user.firstname %></span> <b><%= @user.lastname %></b> </div>
  12. Ruby HTML @user.firstname @user.lastname <div> <span> </span> <b> </b> </div>

    t require "prism" Prism.parse("...") require "nokogiri" Nokogiri::HTML5.fragment("...")
  13. <% if valid? %> <h1 > Title</h1> <% end %>

    <% if valid? %> <h1 > Title</h1> <% end %>
  14. <% 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: "%>"
  15. <% 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: "%>"
  16. <% 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: "%>"
  17. <% 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: "%>"
  18. <% 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: "%>"
  19. <% 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: "%>"
  20. <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>
  21. Refactoring Tag Names <span> → <div> Refactoring Attribute Names data-example-target

    → data-hello-target Refactoring Attribute Values bg-blue-500 → bg-red-500
  22. .heex HTML + EEx (HEEx) .eex Embedded Elixir(EEx) .erb Embedded

    Ruby(ERB) .herb HTML + ERB (Herb) .ex Elixir .rb Ruby
  23. template = ActionView::Template.new( "<h1 > Hello, <%= name %>!", "test.html.erb",

    ActionView::Template::Handlers::ERB, locals: [:name] ) <h1 > Hello, <%= name %>!
  24. 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 %>!
  25. template = ActionView::Template.new( "<h1 > Hello, <%= name %>!", "test.html.herb",

    ActionView::Template::Handlers::Herb, locals: [:name] ) compiled = template.handler.call( template, template.source )
  26. 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
  27. @items = [1, 2, 3] <ul> <li>1</li> <li>2</li> <li>3</li> </ul>

    State View Template Rendered View Log Initial view rendered <ul> <% @items.each do |item| %> <li><%= item %></li> <% end %> </ul>
  28. @items = [1, 2, 3, 4] <ul> <li>1</li> <li>2</li> <li>3</li>

    </ul> State View Template Rendered View Log Initial view rendered @items state changed <ul> <% @items.each do |item| %> <li><%= item %></li> <% end %> </ul>
  29. @items = [1, 2, 3, 4] <ul> <li>1</li> <li>2</li> <li>3</li>

    </ul> State View Template Rendered View Log Initial view rendered @items state changed re-rendering view tracing dependencies <ul> <% @items.each do |item| %> <li><%= item %></li> <% end %> </ul>
  30. <ul> <% @items.each do |item| %> <li><%= item %></li> <%

    end %> </ul> @items = [1, 2, 3, 4] <ul> <li>1</li> <li>2</li> <li>3</li> </ul> State View Template Rendered View Log Initial view rendered @items state changed re-rendering view tracing dependencies render delta for item `4` <li>4</li>
  31. <ul> <% @items.each do |item| %> <li><%= item %></li> <%

    end %> </ul> @items = [1, 2, 3, 4] <ul> <li>1</li> <li>2</li> <li>3</li> </ul> State View Template Rendered View Log Initial view rendered @items state changed re-rendering view tracing dependencies render delta for item `4` <li>4</li>
  32. @items = [1, 2, 3, 4] <ul> <li>1</li> <li>2</li> <li>3</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 <li>4</li> <ul> <% @items.each do |item| %> <li><%= item %></li> <% end %> </ul>
  33. @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>
  34. 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
  35. Check for invalid syntax Check for valid HTML5 A11y Checks-

    Missing alt attributes on <img> XSS Checks - No unsafe interpolation Only one element with the same ID
  36. @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>
  37. Thank you 🙏 t @marcoroth_ M @[email protected] g marcoroth.dev g

    @marcoroth b @marcoroth.dev l /in/marco-roth