Slide 1

Slide 1 text

Marco Roth HTML-Aware ERB: The Path to Reactive Rendering April 24, 2026 - RubyKaigi 2026

Slide 2

Slide 2 text

Marco Roth ! ο‚™ @marcoroth_ ο“Ά @[email protected] 🌐 marcoroth.dev ο‚› @marcoroth Full-Stack Developer & Open Source Contributor ξ™± @marcoroth.dev

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Developer Tooling & Developer Experience

Slide 5

Slide 5 text

ERB

Slide 6

Slide 6 text

Embedded Ruby

Slide 7

Slide 7 text

ERB is versatile

Slide 8

Slide 8 text

<%= @user.firstname %> <%= @user.lastname %>
<%= @user.firstname %> <%= @user.lastname %>

Slide 9

Slide 9 text

HTML+ERB

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Empowering Developers with HTML-Aware ERB Tooling Marco Roth April 16, RubyKaigi 2025

Slide 12

Slide 12 text

HTML+ERB

Slide 13

Slide 13 text

HTML+ERB HTML+ERB

Slide 14

Slide 14 text

Herb

Slide 15

Slide 15 text

Herb Parser

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

Herb Toolchain

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

HTML-Aware ERB: The Path to Reactive Rendering

Slide 21

Slide 21 text

ERB Engines Reactivity What's missing?

Slide 22

Slide 22 text

Herb

Slide 23

Slide 23 text

Rails just works. And it keeps working.

Slide 24

Slide 24 text

Challenge: How do you innovate without breaking what already works?

Slide 25

Slide 25 text

The New Wave of Ruby DX

Slide 26

Slide 26 text

Major Ruby DX Events Since Ruby 3.0 2021 2022 2023 2024 2026 2025/11/06 Ruby 3.0 (release) 2025 repl_type_completor (release) Tomoya Ishida RubyWorld Conference 2025 Steep (v1.0 release) Soutaro Matsumoto debug.gem (release)
 Koichi Sasada RDoc modernization IRB & Reline transformation Tomoya Ishida Mari Imaizumi
 Hitoshi Hasumi Herb (release) Marco Roth Prism (release) Kevin Newton Ruby LSP (release) Vini Stock RBS in Sorbet Alexandre Terrasa
 Alexander Momchilov rbs-inline (release) Soutaro Matsumoto https://github.com/st0012/slides/tree/main/2025-11-06-ruby-world-conference

Slide 27

Slide 27 text

Importance of Shared Infrastructure RubyWorld Conference 2025 2025/11/06 Prism Herb repl_type_completor Ruby LSP Sorbet RuboCop RDoc (Experimental/WIP) Steep RBS https://github.com/st0012/slides/tree/main/2025-11-06-ruby-world-conference

Slide 28

Slide 28 text

But there's one spot that hasn't been touched yet.

Slide 29

Slide 29 text

The View Layer

Slide 30

Slide 30 text

Why Tooling Never Existed?

Slide 31

Slide 31 text

Because there was nothing to build against.

Slide 32

Slide 32 text

There was no parser like Prism to build these kind of tools

Slide 33

Slide 33 text

The question is ...

Slide 34

Slide 34 text

What becomes possible when the ERB engine/runtime also understands HTML?

Slide 35

Slide 35 text

What if we established an HTML+ERB grammar?

Slide 36

Slide 36 text

A formal syntax to define what's valid and invalid

Slide 37

Slide 37 text

Provide structure, hierarchy, and HTML-awareness that tools can understand and reason about

Slide 38

Slide 38 text

Ruby Prism Parser

Slide 39

Slide 39 text

Herb Parser (HTML+ERB structure) + Prism (Ruby grammar) = Full validation before runtime

Slide 40

Slide 40 text

Missing <% end %>? β†’ Herb + Prism catch it at parse time Invalid Ruby syntax? β†’ Prism catches it at parse time Unclosed HTML tag? β†’ Herb catches it at parse time

Slide 41

Slide 41 text

Establishing the grammar is what makes everything else possible.

Slide 42

Slide 42 text

Herb

Slide 43

Slide 43 text

A grammar.

Slide 44

Slide 44 text

A grammar. A parser.

Slide 45

Slide 45 text

A grammar. A parser. A full toolchain for HTML+ERB.

Slide 46

Slide 46 text

This is what was missing.

Slide 47

Slide 47 text

The language definition and the grammer itself

Slide 48

Slide 48 text

One parser. One truth.

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

This is really powerful.

Slide 51

Slide 51 text

If your editor tells you there is an error, you don't have to run the code to find out

Slide 52

Slide 52 text

Because its powered by the same underlying architecture

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

Diagnostics Linter Formatter Document Highlights Support Code Actions Folding Ranges Smart Comment Toggling

Slide 59

Slide 59 text

ERB Engines

Slide 60

Slide 60 text

ERB templates are Ruby

Slide 61

Slide 61 text

ERB templates are Ruby ... almost

Slide 62

Slide 62 text

ERB β†’ Ruby

Slide 63

Slide 63 text

ERB templates ↓ ERB Engine/Compiler ↓ Ruby Code

Slide 64

Slide 64 text

Erubi::Engine

Slide 65

Slide 65 text

Hello, <%= name %>!

Slide 66

Slide 66 text

Hello, <%= name %>!

Hello, <%= name %>!

Slide 67

Slide 67 text

Hello, <%= name %>!

Hello, <%= name %> !

Slide 68

Slide 68 text

Hello, <%= name %>!

Hello, " name "!

Slide 69

Slide 69 text

"

Hello, " name "!

Slide 70

Slide 70 text

"

Hello, " name "!

Slide 71

Slide 71 text

"

Hello, " name "!Hello, ")

Slide 72

Slide 72 text

"

Hello, " name "!Hello, ") add_expression("name")

Slide 73

Slide 73 text

"

Hello, " name "!Hello, ") add_expression("name") add_text("!

Slide 74

Slide 74 text

"

Hello, " name "!Hello, ") add_expression("name") add_text("!

Slide 75

Slide 75 text

add_text("

Hello, ") add_expression("name") add_text("!

Slide 76

Slide 76 text

_buf = ::String.new _buf << '

Hello, ' _buf << ::Erubi.h(name) _buf << '!

Slide 77

Slide 77 text

eval(" _buf = ::String.new _buf << '

Hello, ' _buf << ::Erubi.h(name) _buf << '!

Slide 78

Slide 78 text

eval(" _buf = ::String.new _buf << '

Hello, ' _buf << ::Erubi.h(name) _buf << '! "

Hello RubyKaigi!

Slide 79

Slide 79 text

"

Hello RubyKaigi!Hello, <%= name %>!

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

We want to make our Engine aware of HTML

Slide 82

Slide 82 text

We need this so we can reason about the HTML and to enable reactivity

Slide 83

Slide 83 text

ERB Engine HTML+ERB Engine ↓

Slide 84

Slide 84 text

String Templating HTML Templating ↓

Slide 85

Slide 85 text

::Engine Herb

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

HTML-aware ERB Rendering Engine

Slide 88

Slide 88 text

Erubi::Engine API-compatible

Slide 89

Slide 89 text

Hello, <%= name %>!

Slide 90

Slide 90 text

Herb.parse("

...

")

Slide 91

Slide 91 text

@ 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: ""

Slide 92

Slide 92 text

@ DocumentNode └── children: (1 item) └── @ HTMLElementNode β”œβ”€β”€ open_tag: β”‚ └── @ HTMLOpenTagNode (

) β”‚ β”œβ”€β”€ body: (3 items) β”‚ β”œβ”€β”€ @ HTMLTextNode ("Hello, ") β”‚ β”œβ”€β”€ @ ERBContentNode (<%= name %>) β”‚ └── @ HTMLTextNode ("!") β”‚ └── close_tag: └── @ HTMLCloseTagNode (

)

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

def visit_html_open_tag_node(node) add_text(node.tag_opening) add_text(node.tag_name) add_text(node.tag_closing) end @ HTMLOpenTagNode β”œβ”€β”€ tag_opening: "<" β”œβ”€β”€ tag_name: "h1" └── tag_closing: ">"

Slide 95

Slide 95 text

def visit_html_text_node(node) add_text(node.content) end @ HTMLTextNode └── content: "Hello, "

Slide 96

Slide 96 text

@ 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

Slide 97

Slide 97 text

add_text("<") add_text("h1") add_text(">") add_text("Hello, ") add_expression("name") add_text("!") add_text("")

Slide 98

Slide 98 text

add_text("<") add_text("h1") add_text(">") add_text("Hello, ") add_expression("name") add_text("!") add_text("") add_text("

Hello, ") add_expression("name") add_text("!

") Erubi::Engine Herb::Engine

Slide 99

Slide 99 text

def add_text(text) @tokens << [:text, text] end def add_expression(code) @tokens << [:expr, code] end def add_code(code) @tokens << [:code, code] end

Slide 100

Slide 100 text

add_text("<") add_text("h1") add_text(">") add_text("Hello, ") add_expression("name") add_text("!") add_text("")

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

@tokens = [ ["

Hello, ", :text], ["name", :expr], ["!

", :text], ]

Slide 104

Slide 104 text

@tokens = [ ["

Hello, ", :text], ["name", :expr], ["!

", :text], ]

Slide 105

Slide 105 text

@tokens = [ ["

Hello, ", :text], ["name", :expr], ["!

", :text], ] add_text("

Hello, ") add_expression("name") add_text("!

Slide 106

Slide 106 text

add_text("

Hello, ") add_expression("name") add_text("!

") Erubi::Engine Herb::Engine add_text("

Hello, ") add_expression("name") add_text("!

")

Slide 107

Slide 107 text

_buf = ::String.new _buf << '

Hello, ' _buf << ::Erubi.h(name) _buf << '!

' _buf.to_s (simplified+formatted) _buf = ::String.new _buf << '

Hello, ' _buf << ::Herb.h(name) _buf << '!

' _buf.to_s Erubi::Engine Herb::Engine

Slide 108

Slide 108 text

Gives us all the guarantees the Herb Parser gives us

Slide 109

Slide 109 text

Ensures valid HTML Templates and Syntax

Slide 110

Slide 110 text

String Templating Engine HTML Templating Engine ↓

Slide 111

Slide 111 text

With all this knowledge we can also start to tackle more ambitious things

Slide 112

Slide 112 text

No content

Slide 113

Slide 113 text

Since we control the rendering now

Slide 114

Slide 114 text

We can inject additional HTML markup in development

Slide 115

Slide 115 text

No content

Slide 116

Slide 116 text

No content

Slide 117

Slide 117 text

No content

Slide 118

Slide 118 text

No content

Slide 119

Slide 119 text

No content

Slide 120

Slide 120 text

No content

Slide 121

Slide 121 text

No content

Slide 122

Slide 122 text

No content

Slide 123

Slide 123 text

So where do we go from here?

Slide 124

Slide 124 text

What else do we need?

Slide 125

Slide 125 text

What if we could complete the Hotwire story?

Slide 126

Slide 126 text

And how would that look like?

Slide 127

Slide 127 text

What if Ruby apps could have...

Slide 128

Slide 128 text

β†’ Reactivity β†’ Optimistic UI updates β†’ Offline functionality β†’ Lazy loading driven by the template β†’ Instant interactions without full reloads β†’ Beautiful, modern UX β†’ Component-driven deferred rendering

Slide 129

Slide 129 text

Without having to abandon server-rendered HTML?

Slide 130

Slide 130 text

Without having to write Single Page Applications

Slide 131

Slide 131 text

Without having to give up on full-stack Ruby?

Slide 132

Slide 132 text

Reactivity

Slide 133

Slide 133 text

Production Reactivity Dev-Time Reactivity

Slide 134

Slide 134 text

Production Reactivity

Slide 135

Slide 135 text

The Reactivity Landscape

Slide 136

Slide 136 text

JavaScript: β†’ React (Virtual DOM) β†’ Svelte (compile-time) β†’ Solid (signals)

Slide 137

Slide 137 text

Elixir: Phoenix LiveView + HEEx + LiveEEx β†’ slot-level change tracking

Slide 138

Slide 138 text

PHP: Laravel Livewire + Blade + Blaze

Slide 139

Slide 139 text

What do these frameworks have in common?

Slide 140

Slide 140 text

The engine understands what it's rendering.

Slide 141

Slide 141 text

It has a graph of dependencies.

Slide 142

Slide 142 text

That graph is what enables fine-grained reactivity.

Slide 143

Slide 143 text

We never had something like this in Ruby.

Slide 144

Slide 144 text

Look beyond our own ecosystem.

Slide 145

Slide 145 text

See what others do well. Understand why it works. Bring it back. The Ruby way.

Slide 146

Slide 146 text

No content

Slide 147

Slide 147 text

No content

Slide 148

Slide 148 text

The Goal

Slide 149

Slide 149 text

Keep what's great. Add what's missing. No JavaScript framework.

Slide 150

Slide 150 text

With the goal of making it as backwards compatible as possible

Slide 151

Slide 151 text

So all existing .html.erb files keep working and get an upgrade

Slide 152

Slide 152 text

The Idea

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

    <% @items.each do |item| %>
  • <%= item %>
  • <% end %>
  • 1
  • 2
  • 3
State View Template Rendered View Log Initial view rendered @items state changed re-rendering view @items = [1, 2, 3, 4]

Slide 156

Slide 156 text

  • 1
  • 2
  • 3
State View Template Rendered View Log Initial view rendered @items state changed re-rendering view tracing dependencies
    <% @items.each do |item| %>
  • <%= item %>
  • <% end %>
@items = [1, 2, 3, 4]

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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

    Slide 159 text

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

    Slide 160 text

      <% @items.each do |item| %>
    • <%= item %>
    • <% end %>
    @items = [1, 2, 3, 4]
    • 1
    • 2
    • 3
    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
  • 4
  • Slide 161

    Slide 161 text

      <% @items.each do |item| %>
    • <%= item %>
    • <% end %>
    @items = [1, 2, 3, 4]
    • 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

    Slide 162

    Slide 162 text

    We need to understand what's in the template

    Slide 163

    Slide 163 text

    What Control Flow Structures are used

    Slide 164

    Slide 164 text

    What state is available

    Slide 165

    Slide 165 text

    How data flows

    Slide 166

    Slide 166 text

    The missing piece for this was the grammar and the parser

    Slide 167

    Slide 167 text

    Herb HTML+ERB Parser Prism Ruby Parser

    Slide 168

    Slide 168 text

    How can we make reactivity happen?

    Slide 169

    Slide 169 text

    Source: https://books.writesoftwarewell.com/10/hotwire-handbook/165/traditional-web-architecture

    Slide 170

    Slide 170 text

    app/views/profiles/show.html.erb GET /profiles/:slug ↓ ProfilesController#show ↓

    Slide 171

    Slide 171 text

    β†’ request β†’ controller β†’ layout β†’ view β†’ partials β†’ nested partials

    Slide 172

    Slide 172 text

    Challenge 1: Understand the render graph

    Slide 173

    Slide 173 text

    <%= render partial: "profiles/header", locals: { user: @user } %> <%= render partial: "profiles/topics", locals: { topics: @topics } %> <%= render partial: "profiles/navigation" %>
    <% if @talks.any? %> <%= render partial: "profiles/talks", locals: { talks: @talks } %> <% else %> <%= render partial: "profiles/events", locals: { events: @events } %> <% end %>
    app/views/profiles/show.html.erb

    Slide 174

    Slide 174 text

    <%= render partial: "profiles/header", locals: { user: @user } %> <%= render partial: "profiles/topics", locals: { topics: @topics } %> <%= render partial: "profiles/navigation" %>
    <% if @talks.any? %> <%= render partial: "profiles/talks", locals: { talks: @talks } %> <% else %> <%= render partial: "profiles/events", locals: { events: @events } %> <% end %>

    Slide 175

    Slide 175 text

    <%= render partial: "profiles/header", locals: { user: @user } %> @ ERBRenderNode (location: (1:0)-(1:65)) └── keywords: └── @ RubyRenderKeywordsNode (location: (1:0)-(1:65)) β”œβ”€β”€ errors: [] β”œβ”€β”€ partial: "profiles/header" (location: (1:20)-(1:37)) β”œβ”€β”€ template_path: βˆ… β”œβ”€β”€ inline_template: βˆ… β”œβ”€β”€ body: βˆ… β”œβ”€β”€ plain: βˆ… β”œβ”€β”€ html: βˆ… β”œβ”€β”€ renderable: βˆ… β”œβ”€β”€ collection: βˆ… β”œβ”€β”€ as_name: βˆ… β”œβ”€β”€ spacer_template: βˆ… β”œβ”€β”€ variants: βˆ… └── locals: (1 item) └── @ RubyRenderLocalNode (location: (1:49)-(1:60)) β”œβ”€β”€ errors: [] β”œβ”€β”€ name: "user" (location: (1:49)-(1:54)) └── value: └── @ RubyLiteralNode (location: (1:55)-(1:60)) β”œβ”€β”€ errors: [] └── content: "@user"

    Slide 176

    Slide 176 text

    <%= render partial: "profiles/header", locals: { user: @user } %> @ ERBRenderNode (location: (1:0)-(1:65)) └── keywords: └── @ RubyRenderKeywordsNode (location: (1:0)-(1:65)) β”œβ”€β”€ errors: [] β”œβ”€β”€ partial: "profiles/header" (location: (1:20)-(1:37)) β”œβ”€β”€ template_path: βˆ… β”œβ”€β”€ inline_template: βˆ… β”œβ”€β”€ body: βˆ… β”œβ”€β”€ plain: βˆ… β”œβ”€β”€ html: βˆ… β”œβ”€β”€ renderable: βˆ… β”œβ”€β”€ collection: βˆ… β”œβ”€β”€ as_name: βˆ… β”œβ”€β”€ spacer_template: βˆ… β”œβ”€β”€ variants: βˆ… └── locals: (1 item) └── @ RubyRenderLocalNode (location: (1:49)-(1:60)) β”œβ”€β”€ errors: [] β”œβ”€β”€ name: "user" (location: (1:49)-(1:54)) └── value: └── @ RubyLiteralNode (location: (1:55)-(1:60)) β”œβ”€β”€ errors: [] └── content: "@user"

    Slide 177

    Slide 177 text

    Which files are being rendered when rendering profiles/show?

    Slide 178

    Slide 178 text

    No content

    Slide 179

    Slide 179 text

    Where is this partial being rendered from?

    Slide 180

    Slide 180 text

    No content

    Slide 181

    Slide 181 text

    Since we have the full understanding of the views

    Slide 182

    Slide 182 text

    No content

    Slide 183

    Slide 183 text

    Challenge 2: Trace state dependencies

    Slide 184

    Slide 184 text

    ERB is surprisingly well-suited for this

    Slide 185

    Slide 185 text

    Every ERB tag has a simple contract

    Slide 186

    Slide 186 text

      <% @items.each do |item| %>
    • <%= item %>
    • <% end %>

    Slide 187

    Slide 187 text

    _buf = ::String.new; _buf << '
      ' @items.each do |item| _buf << '
    • ' _buf << item.to_s _buf << '
    • ' end _buf << '
    ' _buf.to_s
      <% @items.each do |item| %>
    • <%= item %>
    • <% end %>

    Slide 188

    Slide 188 text

    _buf = ::String.new; _buf << '
      ' @items.each do |item| _buf << '
    • ' _buf << item.to_s _buf << '
    • ' end _buf << '
    ' _buf.to_s
      <% @items.each do |item| %>
    • <%= item %>
    • <% end %>

    Slide 189

    Slide 189 text

    <%= item %> _buf << (item).to_s

    Slide 190

    Slide 190 text

    <%= .... %> _buf << (....).to_s

    Slide 191

    Slide 191 text

    It takes some state in and it produces a string out

    Slide 192

    Slide 192 text

    When you render an ERB template you get a String as a result

    Slide 193

    Slide 193 text

    This allows us to build out reactivity

    Slide 194

    Slide 194 text

    Challenge 3: Local variables vs. helpers

    Slide 195

    Slide 195 text

    <% if user.present? %> <%= link_to "Show", user_path(user) %> <% end %>

    Slide 196

    Slide 196 text

    <% if user.present? %> <%= link_to "Show", user_path(user) %> <% end %>

    Slide 197

    Slide 197 text

    Locals? Path Helpers? App View Helpers? Action View Tag Helpers?

    Slide 198

    Slide 198 text

    No content

    Slide 199

    Slide 199 text

    Find all locals, instance variables, app helpers, etc. using Prism

    Slide 200

    Slide 200 text

    Action View Helper Registry

    Slide 201

    Slide 201 text

    All references - strict locals - path helpers - app view helpers - Action View Helpers = unresolved references

    Slide 202

    Slide 202 text

    ~ Approximation

    Slide 203

    Slide 203 text

    For reactivity to work, we need to be able to resolve all references

    Slide 204

    Slide 204 text

    But this can be gradual and on a per-route basis

    Slide 205

    Slide 205 text

    or only on the pages where you need high fidelity and high reactivity

    Slide 206

    Slide 206 text

    For unresolved references we can show warnings

    Slide 207

    Slide 207 text

    No content

    Slide 208

    Slide 208 text

    No content

    Slide 209

    Slide 209 text

    No content

    Slide 210

    Slide 210 text

    Define Strict Locals Expose another way for explicit declaration

    Slide 211

    Slide 211 text

    The beautiful thing is that help you resolve these issues because we have the full understanding

    Slide 212

    Slide 212 text

    Challenge 4: Action View Tag Helpers

    Slide 213

    Slide 213 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 214

    Slide 214 text

    What needs to be re-rendered when @post changes?

    Slide 215

    Slide 215 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 216

    Slide 216 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 217

    Slide 217 text

    <%= .... %> _buf << (....).to_s

    Slide 218

    Slide 218 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 219

    Slide 219 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 220

    Slide 220 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 221

    Slide 221 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 222

    Slide 222 text

    We would need to re-render the whole template

    Slide 223

    Slide 223 text

    Herb now supports Action View Tag Helpers detection

    Slide 224

    Slide 224 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 225

    Slide 225 text

    Slide 226

    Slide 226 text

    Slide 227

    Slide 227 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %>

    Slide 228

    Slide 228 text

    This is going to allow fine-grained reactivity

    Slide 229

    Slide 229 text

    No content

    Slide 230

    Slide 230 text

    Challenge 5: Track which reference affect which DOM nodes

    Slide 231

    Slide 231 text

    Reverse Dependency Index

    Slide 232

    Slide 232 text

    <%= @post.title.upcase %>

    <% if @user.admin? %>

    <%= @user.id %>

    <% end %> <%= link_to @post.title, post_path(@post) %>

    Slide 233

    Slide 233 text

    <%= @post.title.upcase %>

    <% if @user.admin? %>

    <%= @user.id %>

    <% end %> <%= link_to @post.title, post_path(@post) %>

    Slide 234

    Slide 234 text

    <%= @post.title.upcase %>

    <% if @user.admin? %>

    <%= @user.id %>

    <% end %> <%= link_to @post.title, post_path(@post) %>

    Slide 235

    Slide 235 text

    <%= @post.title.upcase %>

    <% if @user.admin? %>

    <%= @user.id %>

    <% end %> <%= link_to @post.title, post_path(@post) %>

    Slide 236

    Slide 236 text

    @post => [ "

    <%= @post.title.upcase %>

    ", "<%= link_to @post.title, post_path(@post) %>" ] @user => [ "<% if @user.admin? %>", "

    <%= @user.id %>

    " ]

    Slide 237

    Slide 237 text

    This allows us to compute the minimal changes needed to reflect a state change in the UI

    Slide 238

    Slide 238 text

    What Herb knows: βœ“ ~250 ActionView helpers (via registry) βœ“ Strict locals declarations βœ“ Locals passed through render calls βœ“ Instance variables (@post, @user) βœ“ Constants (Current.user, Post.count)

    Slide 239

    Slide 239 text

    What Herb knows: βœ“ Understand the render graph βœ“ Trace state dependencies βœ“ Resolving Action View Tag Helpers βœ“ Track which variables affect which DOM nodes

    Slide 240

    Slide 240 text

    We aren't quite there yet for production reactivity

    Slide 241

    Slide 241 text

    But this gives us all the primitives and foundation to build out this vision

    Slide 242

    Slide 242 text

    Now all of these primitives need to be wired up

    Slide 243

    Slide 243 text

    Where we can have Phoenix LivewView-like server-side reactivity in Ruby

    Slide 244

    Slide 244 text

    No content

    Slide 245

    Slide 245 text

    Dev-Time Reactivity

    Slide 246

    Slide 246 text

    Vite Dev Server webpack-dev-server

    Slide 247

    Slide 247 text

    Update the view file and the updates get reflected automatically

    Slide 248

    Slide 248 text

    Without a full page reload and re-render of the page

    Slide 249

    Slide 249 text

    Herb Syntax Tree Diffing Engine

    Slide 250

    Slide 250 text

    Herb.diff(old_source, new_source)

    Slide 251

    Slide 251 text

    No content

    Slide 252

    Slide 252 text

    https://github.com/marcoroth/herb/pull/1518

    Slide 253

    Slide 253 text

    Hello World
    ↓

    Slide 254

    Slide 254 text

    Hello World
    Hello Ruby
    ↓

    Slide 255

    Slide 255 text

    Hello World
    Hello Ruby
    ↓

    Slide 256

    Slide 256 text

    <%= title %>

    ↓

    Slide 257

    Slide 257 text

    <%= title %>

    <%= title %>

    ↓

    Slide 258

    Slide 258 text

    <%= title %>

    <%= title %>

    ↓

    Slide 259

    Slide 259 text

    This also works for HTML Elements, ERB Content etc.

    Slide 260

    Slide 260 text

    And you might already be able to see where this is going!

    Slide 261

    Slide 261 text

    Herb Dev Server

    Slide 262

    Slide 262 text

    Hot Reloading in Development

    Slide 263

    Slide 263 text

    herb dev

    Slide 264

    Slide 264 text

    herb dev

    Slide 265

    Slide 265 text

    Uses Herb.diff to get the diff between the old and the new version

    Slide 266

    Slide 266 text

    herb dev

    Slide 267

    Slide 267 text

    No content

    Slide 268

    Slide 268 text

    No content

    Slide 269

    Slide 269 text

    No content

    Slide 270

    Slide 270 text

    No content

    Slide 271

    Slide 271 text

    No content

    Slide 272

    Slide 272 text

    No content

    Slide 273

    Slide 273 text

    No full page refresh

    Slide 274

    Slide 274 text

    Just incremental updates emitted by the Herb Diff Engine

    Slide 275

    Slide 275 text

    Bonus: Compile Time Optimizations

    Slide 276

    Slide 276 text

    These improvements are side-effects of the work we needed for reactivity

    Slide 277

    Slide 277 text

    Compile-time optimizations for Action View Tag helpers

    Slide 278

    Slide 278 text

    <%= turbo_frame_tag dom_id(@post) do %> <%= tag.div data: { controller: "hello" } do %> <%= link_to @post.title, post_path(@post) %> <% end %> <% end %> ↓

    Slide 279

    Slide 279 text

    These are early benchmarks, take them with a grain of salt

    Slide 280

    Slide 280 text

    No content

    Slide 281

    Slide 281 text

    Render Call Optimizations/Inlining

    Slide 282

    Slide 282 text

    No content

    Slide 283

    Slide 283 text

    Some of these results sound too good to be true, but I'm sure there is some performance to be gained

    Slide 284

    Slide 284 text

    Conclusion

    Slide 285

    Slide 285 text

    I hope this talk has given an overview of Herb and its potential

    Slide 286

    Slide 286 text

    And how we are trying to achieve the path Reactivity in ERB

    Slide 287

    Slide 287 text

    The Ruby Prism Parser had a big effect on Ruby internals and the tooling landscape

    Slide 288

    Slide 288 text

    Herb can have a similar effect for HTML Templating and Tooling.

    Slide 289

    Slide 289 text

    No content

    Slide 290

    Slide 290 text

    No content

    Slide 291

    Slide 291 text

    No content

    Slide 292

    Slide 292 text

    With the Herb Toolchain as the foundation reactivity now feels doable.

    Slide 293

    Slide 293 text

    I think reactivity is the missing piece to complete the Hotwire story.

    Slide 294

    Slide 294 text

    So that we can keep writing Ruby

    Slide 295

    Slide 295 text

    and build ambitious web applications with even less complexity

    Slide 296

    Slide 296 text

    Herb v0.10 ships the primitives and foundation for reactivity in ReActionView

    Slide 297

    Slide 297 text

    Acknowledgments

    Slide 298

    Slide 298 text

    Thank you to everyone who tried Herb and reported issues

    Slide 299

    Slide 299 text

    No content

    Slide 300

    Slide 300 text

    github.com/sponsors/marcoroth ο‚›

    Slide 301

    Slide 301 text

    No content

    Slide 302

    Slide 302 text

    https://gem.coop/updates/2026-fellowship/

    Slide 303

    Slide 303 text

    Please try the tools, report issues, and give feedback.

    Slide 304

    Slide 304 text

    No content

    Slide 305

    Slide 305 text

    herb-tools.dev

    Slide 306

    Slide 306 text

    Thank you " ο‚™ @marcoroth_ ο“Ά @[email protected] 🌐 marcoroth.dev ο‚› @marcoroth ξ™± @marcoroth.dev ο‚Œ /in/marco-roth