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 @ SF Bay Area Ruby Meetup, July 2025

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

Event: https://lu.ma/8jcbnfls

Avatar for Marco Roth

Marco Roth

July 22, 2025
Tweet

More Decks by Marco Roth

Other Decks in Programming

Transcript

  1. The Modern View Layer Rails Deserves: A Vision for 2025

    and Beyond Marco Roth July 22, 2025 - SF Bay Area Ruby Meetup
  2. Marco Roth !  @marcoroth_  @[email protected] 🌐 marcoroth.dev 

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

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

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

    link_to post.title, post_path(post) %> </li> <% end %> </ul>
  7. The Modern View Layer Rails Deserves: A Vision for 2025

    and Beyond Marco Roth July 22, 2025 - SF Bay Area Ruby Meetup
  8. I think Rails is in a very good spot to

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

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

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

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

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

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

    valid? %> <h1>Title</h1> <% end %>
  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. <% 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: "%>"
  21. <% 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: "%>"
  22. <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>
  23. Refactoring Tag Names <span> → <div> Refactoring Attribute Names data-example-target

    → data-hello-target Refactoring Attribute Values bg-blue-500 → bg-red-500
  24. The Modern View Layer Rails Deserves: A Vision for 2025

    and Beyond Marco Roth July 22, 2025 - SF Bay Area Ruby Meetup
  25. 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 %>!
  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`
  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. <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>
  33. @items = [1, 2, 3, 4] 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> <ul> <li>1</li> <li>2</li> <li>3</li> </ul>
  34. @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>
  35. 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
  36. 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
  37. @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>
  38. Thank you $  @marcoroth_  @[email protected] 🌐 marcoroth.dev 

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