Rails 8.2 Adopts Herb to Power Reactive Views Without JavaScript

Herb in Rails 8.2: Your ERB views, now HTML-aware - Marco Roth — Ruby on Rails

Ruby on Rails

By parsing templates into addressable DOM slots at compile time, Rails eliminates frontend JavaScript frameworks by running reactive client state, two-way bindings, and optimistic UI updates directly in standard ERB.

AI-assisted summary of Herb in Rails 8.2: Your ERB views, now HTML-aware - Marco Roth — original source. Check the original for full context.

Source published:

Key ideas

  • Rails 8.2 integrates Herb directly into Action View as the default compilation engine for all .html.erb templates, unifying HTML and Ruby into a single semantic AST. (3:00)
  • Herb v0.11 delivers an enhanced Language Server with partial reference tracking, runtime SQL instrumentation that maps database queries back to specific ERB tags, and over 160 new view lint rules. (4:24)
  • Reaction View 0.6 introduces an addressable 'slots' architecture that compiles templates into lightweight dynamic data payloads, enabling surgical DOM updates over HTTP, WebSockets, or SSE without full DOM morphing. (11:47)
  • Client-side state and two-way form bindings run directly in ERB templates without writing JavaScript, bundling Ruby WebAssembly runtimes, or performing Opal code transpilation. (17:48)
  • Reaction View incorporates Rust-powered gems lightningcss-ruby and oxc-ruby to support scoped CSS and scoped TypeScript/JSX scripts inside view partials without adding Node.js to the Rails deployment pipeline. (25:38)

Detailed notes

## Background Presented at Rails World 2026 and published on YouTube by the official Ruby on Rails channel on October 1, 2026, this talk features Marco Roth discussing the integration of Herb into Rails 8.2. According to the video description, Herb delivers an HTML-aware toolchain that comprehends Ruby and HTML together, powering linters, language servers, browser dev tools, and native Action View template compilation.

## TL;DR Marco Roth introduces Herb, an HTML-aware templating engine integrated natively into Rails 8.2 that compiles standard ERB templates with full structural and semantic awareness. Alongside developer tooling enhancements in Herb v0.11—such as LSP diagnostics, runtime query hints, and live-reloading—Roth debuts Reaction View 0.6, an architecture that turns ERB expressions into addressable DOM 'slots.' This slot-based model enables client-side UI state, two-way form binding, declarative async loading, and optimistic updates with automatic rollbacks, entirely bypassing the need for client-side JavaScript frameworks. Looking forward, Roth showcases high-performance Rust tooling via Lightning CSS and Oxc to bring scoped component styles and scripts directly into Rails ERB views.

## Atmosphere & style Marco Roth delivers an engaging, technically grounded presentation marked by modest pride and deep appreciation for the open-source community. The pacing builds methodically from low-level compiler mechanics up to impressive live demonstrations, including a responsive, zero-JavaScript music player and an optimistic chat UI. Watching the session feels like witnessing the realization of a long-standing vision for Rails views, replacing years of frontend friction and boilerplate with an elegant, cohesive developer experience.

## Bulletin - [03:00](https://www.youtube.com/watch?v=REtfdqtW3PY&t=180s) Rails 8.2 integrates Herb directly into Action View as the default compilation engine for all .html.erb templates, unifying HTML and Ruby into a single semantic AST. - [04:24](https://www.youtube.com/watch?v=REtfdqtW3PY&t=264s) Herb v0.11 delivers an enhanced Language Server with partial reference tracking, runtime SQL instrumentation that maps database queries back to specific ERB tags, and over 160 new view lint rules. - [11:47](https://www.youtube.com/watch?v=REtfdqtW3PY&t=707s) Reaction View 0.6 introduces an addressable 'slots' architecture that compiles templates into lightweight dynamic data payloads, enabling surgical DOM updates over HTTP, WebSockets, or SSE without full DOM morphing. - [17:48](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1068s) Client-side state and two-way form bindings run directly in ERB templates without writing JavaScript, bundling Ruby WebAssembly runtimes, or performing Opal code transpilation. - [25:38](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1538s) Reaction View incorporates Rust-powered gems lightningcss-ruby and oxc-ruby to support scoped CSS and scoped TypeScript/JSX scripts inside view partials without adding Node.js to the Rails deployment pipeline.

## Key takeaways - [00:11](https://www.youtube.com/watch?v=REtfdqtW3PY&t=11s) Herb bridges a longstanding tooling gap in the Hotwire ecosystem by parsing HTML and Ruby ERB code into an integrated, structurally aware abstract syntax tree. - [03:00](https://www.youtube.com/watch?v=REtfdqtW3PY&t=180s) In Rails 8.2, Action View routes all .html.erb templates through Herb by default, while non-HTML formats automatically fall back to the legacy ERB template handler. - [04:24](https://www.youtube.com/watch?v=REtfdqtW3PY&t=264s) Herb v0.11 integrates with ActiveSupport::Notifications to project runtime ActiveRecord query diagnostics and N+1 warnings directly as inline hints inside code editors. - [07:17](https://www.youtube.com/watch?v=REtfdqtW3PY&t=437s) Reaction View 0.6 brings fine-grained reactivity to standard Action View templates, providing SPA-like interactivity without requiring frontend JavaScript frameworks. - [11:47](https://www.youtube.com/watch?v=REtfdqtW3PY&t=707s) Herb's slots architecture injects HTML comment delimiters around dynamic ERB tags, allowing individual expressions and partial regions to be addressed and updated surgically in the DOM. - [13:39](https://www.youtube.com/watch?v=REtfdqtW3PY&t=819s) Slot updates are wire-agnostic and rely on just four structural primitives: outputs, slots, collections, and conditionals. - [15:02](https://www.youtube.com/watch?v=REtfdqtW3PY&t=902s) Reaction View uses HTTP content negotiation to return lightweight schema payloads for subsequent dynamic requests while serving standard HTML for initial navigations. - [16:03](https://www.youtube.com/watch?v=REtfdqtW3PY&t=963s) The herb:state directive and data-herb-set attributes manage ephemeral UI state like toggles and accordions locally in the browser, eliminating the need for dedicated Stimulus controllers. - [17:48](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1068s) Reaction View statically compiles a common subset of Ruby view operations into client reactivity models, delivering real-time two-way bindings without Ruby Wasm runtimes. - [19:38](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1178s) When template expressions invoke database queries or complex domain logic, Herb automatically delegates evaluation to background server requests without requiring custom routes or Turbo Frames. - [21:22](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1282s) The built-in <Async> tag defers slow database queries inline, rendering a fallback skeleton immediately before swapping in resolved server data asynchronously. - [21:57](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1317s) Declarative lazy evaluation allows ERB templates to pause server data fetching until an element scrolls into the user's viewport. - [23:42](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1422s) Reaction View achieves optimistic UI updates and deterministic state rollbacks on network failure by maintaining a client-side transaction log of state transitions. - [25:38](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1538s) Rust-backed tooling enables scoped CSS and scoped script compilation natively within Rails, paving the way for offline view synchronization and gem-packaged full-stack components.

## Detailed walkthrough ### [00:11](https://www.youtube.com/watch?v=REtfdqtW3PY&t=11s) Introducing Herb: An HTML-Aware View Layer for Rails 8.2

Marco Roth opens his talk at Rails World 2026 by welcoming the audience and expressing his excitement about the official arrival of Herb in Rails 8.2. Over the past few years as a full-stack developer and open-source contributor, Marco has focused extensively on Hotwire, exploring how far developers can push server-side rendered web applications using the HTML-over-the-wire paradigm. He observed a critical gap in the ecosystem: while backend and deployment tooling continuously advanced, the developer experience and development tooling around Hotwire and the Rails view layer lagged behind.

```mermaid flowchart TD accTitle: Herb View Toolchain Architecture accDescr: Shows how Herb's semantic understanding powers tooling and runtime rendering in Rails 8.2.

subgraph Parser["Herb Parser Core"] AST["HTML + Ruby Semantic AST"] end

subgraph DevTooling["Developer Experience Tools"] Linter["Linter (160+ Rules)"] LSP["Language Server (LSP)"] DevServer["Dev Server (Real-Time Hot Reload)"] BrowserTools["Browser DevTools & Runtime Hints"] end

subgraph Runtime["Action View Runtime (Rails 8.2)"] Compiler["Herb Template Engine"] ActionView["Action View Renderer"] end

AST --> DevTooling AST --> Runtime Compiler --> ActionView ```

Herb was created to solve this by providing a unified templating engine and toolchain that treats HTML and Ruby ERB code as a single cohesive structure rather than disconnected strings. Because Herb possesses full structural and semantic understanding of templates, it enables an entire suite of developer utilities: a dedicated linter, a code formatter, a language server protocol (LSP), in-browser debugging tools, a hot-reloading development server, and now an integrated compiler engine that ships directly inside Rails 8.2 Action View.

- **Browser Debug Tools**: Debuted at Rails World the previous year, giving developers visual view outlines in the browser, direct bi-directional jumps to editor code, and deep introspection into rendered partials. - **Herb Dev Server**: Enables real-time browser updates during local view editing without manual page refreshes, automatically updating multiple instances of partials and components across the page simultaneously.

---

[03:00](https://www.youtube.com/watch?v=REtfdqtW3PY&t=180s) Native Action View Integration in Rails 8.2

In Rails 8.2, Herb transitions from an external tool into an internal compilation engine within Action View. Rails templates historically determine rendering through their action name, format, and template handler. In Rails 8.2, Herb becomes the default engine for any template matching the `.html.erb` pattern (the `html` format paired with the `erb` handler). Templates targeting other formats continue to fall back to the legacy ERB engine for safe backwards compatibility.

To help existing codebases transition safely, Rails 8.2 includes a dedicated compatibility check via a Rake task that developers can run across their application to verify whether all templates are ready for Herb compilation. Applications loading Rails 8.2 defaults enable Herb automatically, but teams can configure the template engine back to standard ERB via application configuration if needed.

```mermaid flowchart LR accTitle: Rails 8.2 Action View Template Handler Selection accDescr: Illustrates how Action View routes.html.erb templates to Herb while falling back to legacy ERB for non-HTML formats.

Tpl["Incoming Template"] --> Check{"Format == HTML & Handler == ERB?"} Check -->|"Yes"| HerbEngine["Herb Compilation Engine (Rails 8.2 Default)"] Check -->|"No"| LegacyERB["Fallback ERB Handler"] ```

---

[04:24](https://www.youtube.com/watch?v=REtfdqtW3PY&t=264s) Herb 0.11: LSP Enhancements, Runtime Instrumentation, and Inline Editor Hints

Launching in tandem with Rails 8.2 is Herb v0.11, the largest release of the toolchain to date, bringing substantial enhancements to editor tooling, diagnostics, and developer feedback loops:

- **Language Server (LSP) Features**: Supports multi-app monorepos via first-class workspace support, component/partial previews, automated code actions (such as extracting view selections into reusable partials), and cross-file diagnostics. The LSP introduces reference tracking for partials, letting human developers and autonomous AI agents locate exactly where partials are invoked across a project to refactor views safely. - **Runtime Instrumentation**: Every ERB tag in a view is instrumented and correlated with standard `ActiveSupport::Notifications`. For instance, Herb hooks into `ActiveRecord` SQL query notifications and maps emitted database queries directly back to the exact ERB tag that triggered them. - **Visual Diagnostics & DevTools**: In the browser, developers can inspect runtime diagnostics, view stack traces, and see visual highlights on the rendered page indicating where each query or diagnostic originated. Herb also provides a redesigned error overlay for hot-reloading runtime errors. - **Inline Editor Hints & Hover Popups**: Diagnostics captured at runtime (such as N+1 queries or heavy SQL statements) are transmitted straight back to the editor as inline hints placed directly next to the offending ERB tags, complete with hover tooltips detailing executed SQL queries. - **Translation Overlays**: Localization keys can be rendered in-place within the editor, showing translated string values until the developer places their cursor on the line to reveal the underlying `t(...)` source code. - **Linting Suite**: Ships with over 160 new lint rules designed to keep view code structurally sound and prevent mistakes when developers or AI coding agents edit templates.

Marco thanks the 31 open-source contributors whose pull requests and feedback shaped Herb v0.11 before pointing attendees to the detailed launch post on the official blog.

---

[07:17](https://www.youtube.com/watch?v=REtfdqtW3PY&t=437s) Reaction View: Bringing Fine-Grained Reactivity to Rails Views

Having placed the Herb compiler at the foundation of Rails apps, Marco explores what becomes possible when the runtime truly understands view HTML. Rails established itself as the premier "one-person framework"—Rails 7 simplified frontend architectures via Hotwire's HTML-over-the-wire approach, and Rails 8 streamlined infrastructure through Solid gems (Solid Queue, Solid Cache, Solid Cable) and SQLite. However, Marco argues that the Rails view layer historically failed to scale in expressiveness alongside the rest of the stack.

To build ambitious, modern, and highly interactive user interfaces without abandoning server rendering, Rails needs "fine-grained reactivity". Taking inspiration from past community efforts like StimulusReflex, modern Rails tools (Turbo and Stimulus), and ecosystems like Phoenix LiveView, Laravel Livewire, Inertia.js, and modern JavaScript SPAs, Marco introduces **Reaction View**.

Following a promotional video showcasing instantaneous DOM morphing and state synchronizations, Marco formally announces the release of **Reaction View 0.6**. Reaction View makes standard Action View templates reactive out of the box without requiring frontend JavaScript single-page application frameworks: "everything a client-side framework does, you can now get without having to adopt one."

- **Zero-Migration Adoption**: Any existing `.html.erb` template is already a valid Reaction View template, operating in server mode by default with standard server-driven reactivity. - **Progressive Interactive Levels**: Reaction View uses a graduated adoption scale across six levels. With version 0.6, the team ships Level 4 and Level 5, introducing fully reactive templates and advanced interactivity, while Level 6 (external component architectures) remains in development.

[11:47](https://www.youtube.com/watch?v=REtfdqtW3PY&t=707s) Introducing Slots: Making ERB Expressions Addressable

To render ERB views on the client side, Marco Roth introduces the foundational architectural concept powering Herb: "slots." In this model, the compiler inspects an ERB template, identifies every ERB tag, and treats each dynamic tag as an addressable slot. When a template contains a loop, the engine recognizes the entire structure as a collection into which dynamic items can flow. Because the Herb toolchain possesses full structural and semantic comprehension of the entire template, it understands not just isolated tags, but the broader intent of the markup.

This deep comprehension enables templates to be compiled into two distinct formats. The first is standard HTML sent over the wire for the initial full page load. The second format is compiled specifically for subsequent dynamic requests, where the server transmits only the updated dynamic values rather than full markup.

Under standard Rails rendering, once an ERB expression like `<%= title %>` compiles into HTML, the browser receives a plain string of text. The browser has no knowledge of where that text originated; the connection back to the source ERB tag is completely detached. Herb solves this by automatically injecting HTML comment boundaries into the markup during the compile step. These comment markers cleanly isolate the evaluated expression so it remains individually addressable within the DOM. By wrapping partials and components in boundary tags that designate their source region, Herb knows the exact file and partial from which an element originated, enabling fine-grained, surgical updates to specific DOM nodes without requiring full page reloads or full DOM morphing operations.

```mermaid flowchart TD accTitle: Dual Compilation and Slot Architecture in Herb accDescr: Diagram showing how an ERB template compiles into either full HTML with comment markers or a lightweight slots payload for subsequent updates.

subgraph Input ["Template Analysis"] ERB["ERB Template<br/>(Tags, Loops, Partials)"] Parser["Herb Semantic Parser<br/>(Extracts Slots & Collections)"] ERB --> Parser end

subgraph Outputs ["Dual Output Compilation"] Initial["Initial Render:<br/>Full HTML with HTML Comment Markers"] Subsequent["Subsequent Requests:<br/>Dynamic Slot Values Only"] Parser -->|"First Page Load"| Initial Parser -->|"Reactive Update"| Subsequent end

subgraph Client ["Browser DOM"] DOM["Addressable DOM Slots<br/>(Surgically updated without morphing)"] Initial --> DOM Subsequent -->|"Patches specific slots"| DOM end ```

- Every individual ERB tag within a view template is registered as an addressable slot. - Loops and iterations are structurally compiled as managed collections. - HTML comments serve as persistent DOM delimiters, preserving the template's source boundaries in the browser. - Surgical slot replacement eliminates the overhead and unpredictability of whole-tree DOM morphing.

---

[13:39](https://www.youtube.com/watch?v=REtfdqtW3PY&t=819s) Wire-Agnostic Updates and the Dev Tools Experience

Because both the server and client maintain an identical map of where dynamic values belong and how to update them, slot updates become completely wire-agnostic. Fresh values can arrive over standard HTTP requests, WebSockets via Action Cable, Server-Sent Events (SSE), or even originate purely within the client itself. Regardless of the delivery transport, the client can apply and reflect the changes instantaneously without re-rendering markup on the server. The entire system is built upon just four fundamental structural primitives: outputs, slots, collections, and conditionals.

To demonstrate this in practice, Marco walks through a template rendering the current server time via `<%= Time.now %>`. Adding a button with a `data-herb-action="refresh"` attribute—intentionally designed to mimic the familiar syntax of Stimulus's `data-action`—wires up the interaction. When the user clicks the button, the view triggers a targeted live update in the browser rather than a standard navigation. Herb's companion browser developer tools highlight precisely which slots updated in real time, making the flow of dynamic data completely transparent during development.

- Transport versatility: slot updates can be delivered via HTTP, Action Cable, Server-Sent Events, or client-side evaluation. - The reactivity model relies entirely on four primitives: outputs, slots, collections, and conditionals. - Event binding follows a familiar declarative syntax with attributes like `data-herb-action="refresh"`. - Dedicated browser dev tools provide visual indicators pinpointing exactly which template slots update on user interaction.

---

[15:02](https://www.youtube.com/watch?v=REtfdqtW3PY&t=902s) Under the Hood: The Slots Format and Schema Negotiation

Behind the scenes of that refresh interaction, the browser issues a standard HTTP `GET` request to the exact URL the user is currently visiting, while attaching a specialized `Accept` header requesting the Herb slots format. On the Rails side, the controller action remains entirely standard and empty:

```ruby def index end ```

Normally, Rails implicitly responds to this request with the HTML format. ReactionView hooks into this lifecycle by registering an implicit response format for slots. While the user's initial browser navigation receives standard HTML, any subsequent background request bearing the slot-specific `Accept` header is routed through the slot compiler instead.

The payload returned by this format is essentially a direct representation of the template's schema. If a template defines five slots, the response contains a compact payload indexing those five slots. Because the client-side runtime already possesses the identical schema, it simply reads the values keyed to slot zero, slot one, and so forth, patching them directly into their corresponding DOM positions.

| Request Type | Accept Header | Render Engine | Payload Received by Browser | |:--- |:--- |:--- |:--- | | **Initial Navigation** | `text/html` | Action View / Herb | Full HTML markup containing slot comment delimiters | | **Subsequent Action** | Herb Slots Accept Header | ReactionView Slots Formatter | Lightweight schema payload containing only dynamic slot values |

- Controller actions require zero custom boilerplate; Rails routes and responds implicitly via content negotiation. - ReactionView introduces a dedicated slots format alongside standard Rails formats. - Payloads omit repetitive HTML tags, sending only the raw data mapped to specific slot indices. - The client runtime uses its local copy of the template schema to bind incoming slot values to their matching DOM comment boundaries.

---

[16:03](https://www.youtube.com/watch?v=REtfdqtW3PY&t=963s) Client-Side UI State Without Stimulus Boilerplate

While slot refreshing provides efficient server round-trips, modern applications demand immediate UI state—ephemeral values that govern visibility, toggles, and modal states without touching a database. Marco emphasizes his architectural philosophy: "not every UI interaction itself needs to go and do a server round trip." Traditionally, Rails developers reach for Stimulus, creating a custom JavaScript controller and wiring up targets, values, and actions just to toggle an element.

Herb bypasses this boilerplate by letting developers write pure ERB. Drawing inspiration from strict locals syntax, Herb introduces a template directive for state:

```erb <%# herb:state, show: false %> ```

Here, `show` is defined as a reactive client-side boolean state initialized to `false`. Inside the template, developers write standard Ruby conditional checks like `<% if show? %>`. Because the Herb compiler understands that `show` is a declared state variable, it recognizes what DOM elements depend on it.

To manipulate that state, developers attach declarative attributes to their HTML elements:

```html <button data-herb-set="show = true">Show Details</button> ```

Because the target element is a `<button>`, Herb implicitly binds to the `click` event, matching the ergonomic shortcuts popularized by Stimulus. For boolean values, a toggle helper can alternate the state between `true` and `false`. The entire conditional branch appears and disappears purely on the client side without executing a single network request or requiring a dedicated JavaScript file.

- Ephemeral UI interactions (toggles, accordions, dialogs) remain entirely on the client, avoiding unnecessary server round-trips. - The `herb:state` directive defines local reactive variables using syntax inspired by Rails strict locals. - Standard Ruby predicate methods and conditionals (`if show?`) are understood by the compiler and converted into client-side visibility toggles. - The `data-herb-set` attribute enables declarative state modification directly in HTML, supporting explicit values and toggle actions.

---

[17:48](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1068s) Two-Way Data Binding Without JavaScript or Ruby Wasm

Herb extends this declarative model to full two-way data binding for form inputs. Because Herb parses both HTML and Ruby simultaneously, it can inspect form elements directly. When an input's `value` attribute consists solely of an ERB expression linked to a declared state—such as `<input value="<%= message %>">` where `message` is defined via `<%# herb:state, message: "" %>`—the compiler binds the input to the state during compilation.

```erb <%# herb:state, message: "" %>

<input type="text" value="<%= message %>"> <p>Character count: <%= message.length %></p> ```

As the user types into the text input, the runtime updates the `message` state on every keystroke. Herb's reactivity model recognizes that `<%= message.length %>` depends on that state and recalculates the length instantly on the client.

Marco clarifies that this does not involve heavy machinery: there is no custom JavaScript written by the developer, no Ruby-to-JavaScript transpilation (such as Opal), and no Ruby WebAssembly virtual machine bundled into the client. Instead, ReactionView analyzes the abstract syntax tree (AST) of the ERB template, identifies the limited subset of standard Ruby methods commonly used in view layers (like `.length` or string operations), and compiles those operations directly into an efficient client-side activity model.

- Declaring a state variable and assigning it to an `<input value="...">` creates an automatic two-way binding during compilation. - Dependent Ruby view expressions update reactively in the DOM as input values change. - The system does **not** rely on client-side Ruby WebAssembly runtimes or Ruby-to-JS transpilers. - ReactionView statically translates the common subset of Ruby view operations into optimized client-side reactivity at compile time.

---

[19:38](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1178s) Hybrid Reactivity: Automatic Server Fallbacks

When view logic exceeds the capabilities of client-side static analysis—such as executing complex domain logic or querying an ActiveRecord database—Herb seamlessly falls back to the server. Marco illustrates this with a dynamic search query:

```erb <%# herb:state, q: "" %>

<input type="text" value="<%= q %>">

<div> <%= Topic.search(q) %> </div> ```

The compiler identifies that `q` is a reactive client-side state, and it detects that the ERB tag invoking `Topic.search(q)` directly depends on that state. However, because `Topic.search` represents a database query that cannot run inside the browser, the compiler wires the dependency to execute a background network request whenever `q` changes.

The server evaluates the query, serializes the updated results into the slots format, and returns the payload to the browser. The client runtime then patches the returned values directly into the appropriate slot in the DOM.

Marco reflects on the elegance of this workflow: "And if you look at this, that's just how you write ERB. I've been writing ERB the whole time, but it's just reactive now, which is super cool." Developers get fine-grained, reactive UI updates without needing to configure Turbo Frames, create custom Turbo Stream responses, synchronize DOM element IDs, or write dedicated controller actions and routes. The server and client negotiate updates transparently through the shared template schema.

- Complex expressions and database queries automatically trigger smart background server requests. - The client runtime handles data transport, slot decoding, and DOM patching automatically. - Eliminates the manual wiring typically required for Turbo Frames, Turbo Streams, and dedicated search endpoints. - Developers write standard, idiomatic ERB while Herb infuses it with full client-server reactivity.

---

[21:22](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1282s) Deferring Slow Queries with the `<Async>` Tag

Marco transitions to another common performance hurdle in Rails views: lazy loading slow database queries to avoid blocking the initial page render. In standard Rails development, deferring an expensive query usually requires extracting the markup into a dedicated partial, registering a separate route, and wrapping the content in a `<turbo-frame>` configured with a `src` attribute.

Herb streamlines this process by introducing a built-in `<Async>` component tag with a capital "A", adopting component ergonomics familiar to developers from modern UI ecosystems like React:

```html <Async> <%= SlowMetric.calculate_expensive_stats %> </Async> ```

Instead of requiring separate endpoints and fragment extractions, the `<Async>` tag allows developers to defer expensive view segments inline, treating asynchronous rendering as a first-class feature of the template itself.

- Eliminates the need to extract lazy-loaded markup into separate partials and standalone controller routes. - The capitalized `<Async>` tag brings modern component ergonomics directly into standard ERB views. - Deferring slow queries and metrics is declared inline at the template layer.

[21:57](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1317s) Async and Lazy Declarative Rendering

Marco Roth explains how the Herb compiler unlocks asynchronous and lazy view rendering directly within templates. When a developer marks a template section as async, the compiler recognizes the structure, identifies the fallback skeleton, and renders that lightweight placeholder immediately on the initial request. As soon as the initial HTML arrives in the client browser, the client runtime identifies the async boundary, reaches back out to the server in the background, fetches the actual data payload, and seamlessly swaps it into place.

```mermaid flowchart TD accTitle: Declarative Async and Lazy View Pipeline accDescr: Diagram showing initial skeleton rendering on the server followed by client-side asynchronous fetching or viewport intersection triggers.

subgraph InitialRender["1. Initial Server Render"] A["Compiler parses ERB template"] --> B["Detects async or lazy block"] B --> C["Renders lightweight fallback skeleton"] end

subgraph ClientDelivery["2. Client Browser"] C --> D["HTML displayed instantly in browser"] D --> E{"Trigger Condition"} E -->|"Immediate Async"| F["Background fetch to server"] E -->|"Lazy (Viewport)"| G["Waits until scrolled into view"] G --> F end

subgraph ServerHydration["3. Server Data Fetch"] F --> H["Action View server responds with payload"] H --> I["DOM seamlessly swaps skeleton for content"] end ```

The exact same mental model extends to lazy evaluation. Rather than kicking off a network request the moment the page mounts, a lazy block pauses until the element actually enters the user's viewport. Marco steps through a live demo: as the user loads the page, initial content is immediately present, and only when scrolling all the way to the bottom does the lazy block begin fetching and streaming in the remaining values. Roth emphasizes that this is achieved through pure declarative state in ERB. He notes candidly that he is completely open to community feedback regarding syntax and naming: "I'm open to feedback about naming this because this is just the way I came up with, but if you have any more idea about syntax and naming on this, I'm open to feedback here."

[23:06](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1386s) Building Rich UIs in ERB: The Zero-JavaScript Music Player

To demonstrate what this architecture achieves in practice without writing frontend glue code, Roth unveils a fully functioning music player and library interface built entirely in ERB. When a user clicks through the catalog, details appear instantaneously. Skeletons handle in-flight state gracefully, and users can play back audio tracks, pause, toggle controls, and close panels without lag.

Roth highlights that the entire interactive music player application runs on roughly 100 lines of ERB with absolutely zero custom JavaScript. By shifting structural awareness into the compilation step, standard Rails templates can replicate the polished, responsive experiences that traditionally required dedicated Single Page Application (SPA) frameworks or heavy client scripts.

[23:42](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1422s) Optimistic UI and State Rollbacks

Roth advances to one of the most demanding problems in web interfaces: optimistic UI. Because the Herb compiler understands the complete template tree ahead of time, that markup representation can be pushed down to the client runtime to perform real client-side rendering. Demonstrating a real-time chat application, Roth shows messages appearing in the stream instantly upon submission. If a network transmission fails, the client marks the failure immediately, allowing users to hit retry and resend without losing their input.

Editing and deleting messages also happen locally without a server round trip because toggling states and hiding or showing nodes are handled directly in the browser runtime. When a user stars a message, the action renders instantly in the UI while queuing a background HTTP request. If that request fails, the runtime references an internal state ledger and rolls back to the last known good state. By keeping a comprehensive log of all historical state transitions, the system can deterministically travel backward and forward across UI mutations.

- Chat messages render on the client immediately without waiting for server confirmation. - Destructive and update actions, such as editing or deleting, manipulate local DOM state directly without network round trips. - Network requests are placed into an asynchronous queue. - If an operation fails, the runtime rewinds the UI to the last confirmed good state using an event log of state transitions.

[24:46](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1486s) Reaction View Runtime and Low-Level JavaScript Interoperability

While Roth underscores that developers can build remarkably ambitious, interactive applications using pure ERB, he acknowledges that complex real-world projects frequently require integration with NPM packages or custom DOM event listeners. To bridge this gap, Reaction View exposes a low-level JavaScript API that interfaces directly with the client runtime.

Getting started with Reaction View requires minimal ceremony: developers simply add the `reaction_view` gem to their Gemfile and run the included install generator. Full documentation is hosted at `reactionview.dev`, and the source code and interactive demonstrations are available open source at `demo.reactionview.dev` and on GitHub.

[25:38](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1538s) Scoped Styles and Scripts: Rust Tooling in the Rails View Layer

Looking toward future developments, Roth addresses persistent friction points in frontend Rails development: managing scoped CSS and component-level JavaScript without introducing heavy toolchains. To prevent style bleed across view templates, Reaction View introduces `<style scoped>`, enabling developers to embed component-specific style tags directly inside ERB partials. Under the hood, preprocessing is handled by `lightningcss-ruby`, a newly packaged native gem binding the Rust-based Lightning CSS engine to parse, scope, and minify styles on the fly without leaking outside the partial boundary.

Roth envisions an equivalent pattern for JavaScript using `<script scoped>`. To power this without introducing Node.js into the Rails deployment pipeline, the team created `oxc-ruby`. Built upon Oxc—the high-performance Rust JavaScript tooling suite powering modern frontend build tools like Vite and Rollup—the gem allows Rails developers to write modern TypeScript or JSX directly within their views. It compiles and bundles code natively while staying fully compatible with Rails import maps.

```mermaid flowchart LR accTitle: High-Performance Rust Tooling in Rails Views accDescr: Flowchart illustrating how ERB partials leverage Rust-backed gems to compile scoped styles and scripts without Node.js.

subgraph ERB["ERB Template"] CSS["<style scoped>"] JS["<script scoped> (Future)"] end

subgraph RustTooling["Rust Gems (No Node.js Required)"] LCSS["lightningcss-ruby\n(Lightning CSS)"] OXC["oxc-ruby\n(Oxc Engine)"] end

subgraph Outputs["Engine Output"] OutCSS["Scoped, isolated partial CSS"] OutJS["Bundled TypeScript / JSX\n(Import Map Compatible)"] end

CSS --> LCSS --> OutCSS JS --> OXC --> OutJS ```

Beyond styling and scripting, Roth points to several major frontiers unlocked by having a DOM-aware view layer: - **True Offline Mode:** Because full template markup structures are precompiled and pushed down to the client runtime, applications can run fully offline and synchronize later. - **Side-by-Side React Components:** Native ERB templates and React components can coexist seamlessly in the same view trees. - **Encapsulated Gem Components:** Third-party Ruby gems can package and distribute full-stack components that encompass coordinated server-side and client-side behaviors, rendered natively through the Herb engine.

[27:29](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1649s) The Future of Full-Stack Rails and hotwire Evolution

Reflecting on the overarching vision, Roth expresses deep satisfaction with where Herb and Reaction View are heading, noting that developers can once again write classic Ruby and ERB views while delivering fully reactive, dynamic interfaces. Rather than forcing teams into an all-or-nothing dichotomy between backend monolithic views and JavaScript-heavy SPAs, this model offers a continuous spectrum: teams can adopt as much or as little JavaScript as their product requires without abandoning full-stack Rails idioms.

Roth notes that Reaction View represents what he always envisioned StimulusReflex could become, but implemented cleanly from the view compiler up. By expanding Hotwire's capabilities while preserving the simplicity Rails has championed for two decades, developers gain the freedom to introduce React or custom client components when strictly necessary without sacrificing the cohesive developer experience of Rails. He emphasizes that bringing Herb into Rails 8.2 establishes the bedrock, but its long-term success depends entirely on community collaboration, real-world stress testing, and honest feedback from skeptics and early adopters alike.

[29:25](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1765s) Acknowledgements and Community Links

Roth wraps up his Rails World presentation by expressing gratitude to the open-source community. He thanks the contributors who submitted pull requests, triaged bugs, tested experimental features, and shaped the projects into production-ready software. He also thanks the sponsors whose financial support sustained his open-source work across the toolchain.

He encourages attendees to try out the gems, star the Herb and Reaction View repositories on GitHub, submit issues, and explore the updated documentation and live application examples available at `reactionview.dev` and `herb-tools.dev`.

## Notable quotes > [00:00](https://www.youtube.com/watch?v=REtfdqtW3PY&t=0s) Herb has full semantic and structural understanding of your ERB templates.

> [00:00](https://www.youtube.com/watch?v=REtfdqtW3PY&t=0s) Now its engine renders and compiles ERB views inside Action View itself.

> [07:17](https://www.youtube.com/watch?v=REtfdqtW3PY&t=437s) everything a client-side framework does, you can now get without having to adopt one. — Marco Roth

> [13:39](https://www.youtube.com/watch?v=REtfdqtW3PY&t=819s) The entire system is built upon just four fundamental structural primitives: outputs, slots, collections, and conditionals. — Marco Roth

> [15:02](https://www.youtube.com/watch?v=REtfdqtW3PY&t=902s) The payload returned by this format is essentially a direct representation of the template's schema. — Marco Roth

> [16:03](https://www.youtube.com/watch?v=REtfdqtW3PY&t=963s) not every UI interaction itself needs to go and do a server round trip. — Marco Roth

> [19:38](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1178s) And if you look at this, that's just how you write ERB. I've been writing ERB the whole time, but it's just reactive now, which is super cool. — Marco Roth

> [21:57](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1317s) I'm open to feedback about naming this because this is just the way I came up with, but if you have any more idea about syntax and naming on this, I'm open to feedback here. — Marco Roth

> [23:06](https://www.youtube.com/watch?v=REtfdqtW3PY&t=1386s) By shifting structural awareness into the compilation step, standard Rails templates can replicate the polished, responsive experiences that traditionally required dedicated Single Page Application (SPA) frameworks or heavy client scripts. — Marco Roth

## Mentioned Marco Roth, Ruby on Rails, Rails 8.2, Rails World 2026, Herb, ERB, Action View, Hotwire, Turbo, Stimulus, StimulusReflex, Phoenix LiveView, Laravel Livewire, Inertia.js, Reaction View, ActiveSupport::Notifications, ActiveRecord, Solid Queue, Solid Cache, Solid Cable, SQLite, Ruby Wasm, Opal, React, lightningcss-ruby, Lightning CSS, oxc-ruby, Oxc, Vite, Rollup, Shopify

## Video description Rails 8.2 ships with Herb, a toolchain that understands your HTML and Ruby together. At Rails World 2026, Marco Roth explains what Herb is and what it adds to the Rails view layer.

Herb has full semantic and structural understanding of your ERB templates. That makes it possible to build a linter, a formatter, a language server, browser dev tools, and a dev server that updates your views and partials in real time without a page refresh. Now its engine renders and compiles ERB views inside Action View itself. Marco walks through what ships in Rails 8.2 and what an HTML-aware view layer makes possible next.

Links: Presentation: https://speakerdeck.com/marcoroth/herb-in-rails-8-dot-2-your-erb-views-now-html-aware-at-rails-world-2026-austin-texas https://herb-tools.dev https://reactionview.dev https://github.com/marcoroth/herb https://github.com/marcoroth/reactionview https://herb-tools.dev/blog/whats-new-in-herb-v0-11

***

Thank you Shopify for sponsoring the editing and post-production of these videos. Check out insights from the Engineering team at: https://shopify.engineering/

#RailsWorld #RubyOnRails #Rails82 #Herb #ERB #Hotwire

[Source](https://www.youtube.com/watch?v=REtfdqtW3PY)

中文简报

Rails 8.2 引入 Herb,无需 JavaScript 实现响应式视图 通过在编译时将模板解析为可寻址的 DOM 槽位,Rails 直接在标准 ERB 中运行响应式客户端状态、双向绑定和乐观 UI 更新,从而摆脱前端 JavaScript 框架。 Marco Roth 介绍了 Herb,这是一款原生集成至 Rails 8.2 的 HTML 感知模板引擎,能够在充分掌握结构与语义的前提下编译标准 ERB 模板。除了 Herb v0.11 中的开发者工具增强功能(如 LSP 诊断、运行时查询提示和实时重载)之外,Roth 还推出了 Reaction View 0.6,该架构可将 ERB 表达式转化为可寻址的 DOM“槽位”。这种基于槽位的模型支持客户端 UI 状态、表单双向绑定、声明式异步加载以及带自动回滚的乐观更新,完全无需客户端 JavaScript 框架。展望未来,Roth 展示了通过 Lightning CSS 和 Oxc 构建的高性能 Rust 工具链,旨在将作用域组件样式和脚本直接引入 Rails ERB 视图中。 Rails 8.2 将 Herb 直接集成到 Action View 中,作为所有 .html.erb 模板的默认编译引擎,把 HTML 和 Ruby 统一为一个语义 AST。 (3:00) Herb v0.11 提供了增强型语言服务器(支持局部引用跟踪)、可将数据库查询映射回特定 ERB 标签的运行时 SQL 插桩,以及超过 160 条全新的视图 lint 规则。 (4:24) Reaction View 0.6 引入了可寻址的“槽位”架构,将模板编译为轻量级动态数据载荷,从而能通过 HTTP、WebSocket 或 SSE 实现精准的 DOM 更新,而无需进行完整的 DOM 变形替换。 (11:47) 客户端状态和表单双向绑定可直接在 ERB 模板中运行,无需编写 JavaScript、打包 Ruby WebAssembly 运行时或执行 Opal 代码转译。 (17:48) Reaction View 整合了基于 Rust 的 gem(lightningcss-ruby 和 oxc-ruby),以支持在视图局部中引入作用域 CSS 和作用域 TypeScript/JSX 脚本,且无需在 Rails 部署流程中添加 Node.js。 (25:38)

Reading summary

At Rails World 2026, Marco Roth presented Herb's integration as the default template engine for HTML ERB views in Rails 8.2, introducing a semantic parser that turns Action View into a reactive, DOM-aware presentation layer without requiring client-side single-page application frameworks. Native Rails 8.2 Integration and Toolchain Upgrades (0:11) In Rails 8.2, Action View replaces the legacy ERB handler with Herb as the default compiler for all `.html.erb` templates, while non-HTML formats retain standard ERB processing for backwards compatibility. A companion Rake task provides automated compatibility checks for existing codebases. Because Herb treats HTML and embedded Ruby as a unified abstract syntax tree (AST) rather than arbitrary strings, it introduces deep developer tooling alongside runtime rendering. Herb v0.11, launched alongside Rails 8.2 with contributions from 31 open-source contributors, expands this toolchain with over 160 lint rules, multi-app workspace support, partial reference tracking, and inline editor diagnostics. By linking into ActiveSupport notifications, Herb correlates runtime database queries directly with the exact ERB tags that caused them, surfacing N+1 warnings and query details directly inside the code editor and browser inspection tools. Addressable Slots and Dual-Format Schema Negotiation (11:47) To render dynamic views without full-page reloads or complete DOM morphing, Herb treats individual ERB tags as addressable 'slots' and iterative loops as managed collections. During compilation, Herb injects HTML comment delimiters into markup boundaries, preserving the template's source identity in the browser DOM. The system relies on four fundamental primitives: outputs, slots, collections, and conditionals. This architecture enables dual-output compilation. The initial page visit delivers complete HTML with boundary markers. Subsequent dynamic requests—such as clicking an element with a declarative `data-herb-action="refresh"` attribute—send standard HTTP requests containing a custom Accept header. Rather than executing custom controller code, Rails returns a compact payload containing only the modified slot indices, which the client patches into the matching DOM boundaries. Updates are wire-agnostic, allowing data delivery via HTTP, Action Cable WebSockets, Server-Sent Events, or client-side evaluations. Declarative Client State Without Frontend Frameworks (16:03) To address ephemeral interface changes—such as toggling dialogs or accordions—without network round trips or boilerplate Stimulus controllers, Herb introduces client-side state directives written in pure ERB. Defining `<%# herb:state, show: false %>` initializes reactive state, allowing standard Ruby conditionals like `<% if show? %>` to be compiled directly into client-side visibility toggles manipulated via attributes like `data-herb-set="show = true"`. Herb also supports two-way input binding when form fields reference declared state. As users type, the client runtime updates dependent expressions immediately. Roth emphasized that this evaluation avoids bundling Ruby WebAssembly runtimes or Ruby-to-JavaScript transpilers like Opal. Instead, the Reaction View runtime analyzes the template AST and statically maps a common subset of Ruby view operations, such as string operations and `.length`, directly into optimized client reactivity. Hybrid Server Fallbacks, Inline Async, and Optimistic UI (19:38) When dynamic ERB logic requires operations that cannot execute in the browser—such as database searches via `Topic.search(q)`—Herb creates an automatic server fallback. Changing the client state automatically queues a background request to the server, which runs the query and returns updated slot data without requiring developers to manually configure Turbo Frames, Turbo Streams, or distinct routes. Expensive view fragments can also be deferred inline using the capitalised `<Async>` tag, which delivers a lightweight fallback skeleton on the first render before asynchronously fetching and swapping the deferred content. With Reaction View 0.6, the runtime can also manage optimistic UI states. In a chat application demo, messages render instantly upon submission while background network requests queue in the background. If a server request fails, the client uses an internal state ledger of historical UI mutations to roll back deterministically to the last confirmed good state. Scoped Component Tooling via Native Rust Bindings (25:38) To manage scoped styling and scripting within ERB without adding Node.js dependencies, the project incorporates Rust-backed gems. The `lightningcss-ruby` gem processes `<style scoped>` tags embedded inside ERB partials, minifying and isolating CSS rules locally. A planned `<script scoped>` feature uses `oxc-ruby`—based on the Oxc engine—to bundle component-level TypeScript or JSX natively while retaining compatibility with Rails import maps. Roth noted that this compilation model positions Rails to support offline application modes, side-by-side React component embedding, and distributable full-stack gems. However, he acknowledged that long-term success requires community validation, stress testing, and syntax feedback, noting that low-level JavaScript APIs remain available when developers must interface directly with external DOM libraries or NPM packages. Roth presents Herb and Reaction View as an evolutionary middle ground that restores classic Ruby templating idioms while offering modern reactive UI ergonomics. Whether this hybrid compilation model can fully displace complex JavaScript frameworks depends on broader community adoption, real-world edge-case validation, and feedback on template syntax.

Source quotes

[0:00] Herb has full semantic and structural understanding of your ERB templates. [0:00] Now its engine renders and compiles ERB views inside Action View itself. [7:17] Marco Roth: everything a client-side framework does, you can now get without having to adopt one. [13:39] Marco Roth: The entire system is built upon just four fundamental structural primitives: outputs, slots, collections, and conditionals. [15:02] Marco Roth: The payload returned by this format is essentially a direct representation of the template's schema. [16:03] Marco Roth: not every UI interaction itself needs to go and do a server round trip. [19:38] Marco Roth: And if you look at this, that's just how you write ERB. I've been writing ERB the whole time, but it's just reactive now, which is super cool. [21:57] Marco Roth: I'm open to feedback about naming this because this is just the way I came up with, but if you have any more idea about syntax and naming on this, I'm open to feedback here. [23:06] Marco Roth: By shifting structural awareness into the compilation step, standard Rails templates can replicate the polished, responsive experiences that traditionally required dedicated Single Page Application (SPA) frameworks or heavy client scripts.

Original transcript

[0:06] [music] [0:11] >> Thank you. Good afternoon. [0:13] Hi, my name is Marco. I'm here today to [0:16] talk to you about Urban Rails 8.2. [0:19] And I'm really happy to see that this [0:21] kind of came to be and I want to show [0:23] you what this will enable us to do in [0:25] the future. [0:28] If you haven't met, I'm Marco. I'm full [0:30] stack developer and open source [0:31] contributor and [0:33] over the past few years I've been [0:34] working on Hotwire. [0:37] And I wanted to [0:39] see um [0:41] how far I can push things [0:43] with this approach using the HTML of the [0:45] wire [0:46] and the server side rendered web [0:48] applications. [0:50] For the last few years I've been on a [0:51] mission to improve the development [0:52] tooling and developer experience in the [0:54] Hotwire ecosystem and more recently in [0:56] the Rails view layer. [0:59] That's how Herb came to be. [1:02] And today I want to show you what Herb [1:05] will enable [1:07] in Rails 8.2 [1:08] and what it will enable Rails 8.2 to do. [1:14] So, [1:15] welcome to Herb in Rails 8.2. Your ERB [1:17] views now HTML aware. [1:20] I want to talk about what is Herb, [1:22] what ships in 8.2, and [1:25] what does this enable. [1:28] First, what is Herb? [1:30] Herb is a modern templating language for [1:32] your HTML ERB you already have. [1:35] And it's one language and one tool chain [1:36] that understands your HTML and Ruby [1:38] together. [1:41] Herb has full semantic and structural [1:43] understanding of your views, which [1:45] allows us to build a linter, a [1:47] formatter, a language server, an engine, [1:50] dev tools, and also dev server. [1:54] The engine is what's shipping in Rails [1:55] 8.2 to render and compile your ERB [1:57] views. [1:59] Last year at Rails World, we introduced [2:01] these [2:02] Herb debug tools that you can [2:04] enable in your browser. [2:06] And they allow you to show the outlines [2:08] of the views, give you the link between [2:11] your editor and the browser, and just [2:14] give you this interaction and [2:15] introspection into the views and how [2:17] they work. [2:19] This is already really powerful and kind [2:21] of already showed that this is kind of [2:22] useful, but we can take this even [2:24] further. [2:27] Recently, we also shipped the Herb dev [2:28] server, which [2:30] allows you to work in development. So, [2:34] when you edit your views, you get [2:35] instant updates in your browser, so you [2:37] don't have to refresh the page every [2:38] time. [2:40] This also works for partials, components [2:42] that you have in your views. So, [2:43] whenever you have multiple partials on [2:45] the same page, they all refresh and [2:48] update in real time. [2:52] So, Herb helps you in development, in [2:54] CI, in your editor, and now also with [2:57] rendering your views. [2:59] So, I want to talk about what it now [3:00] ships in 8.2. [3:04] Action View is what enables this. [3:07] And funny enough, this kind of [3:11] um logo from Action from a few years [3:12] ago, I guess was kind of hinting at Herb [3:14] already. We like the logo. [3:16] But, um [3:18] So, in Action View, we have a templates, [3:20] right? And these are made up of the [3:23] action itself, [3:25] the formats, [3:26] and also the handler. [3:29] And in Rails 8.2, if the [3:32] default format for [3:34] So, for the HTML format, the default is [3:36] going to be Herb. [3:38] So, any [3:39] format that has the HTML formats and any [3:42] template that has the ERB handler is [3:43] going to be served and compiled by Herb. [3:47] And for anything else, we're going to [3:48] use fall back to ERB, which is the [3:51] engine that was [3:52] has been used before. [3:55] There's also a new check [3:57] and rake task in the 8.2 that you can [3:59] run in your app to figure out if it's [4:01] compatible. And whenever this passes, [4:03] you are safe to kind of opt into the new [4:06] compilation. [4:08] There's also a new formats to or new [4:11] configuration to enable this. [4:13] When you kind of load the defaults in [4:15] 8.2, it will set this to Herb by [4:17] default. [4:18] And if you want to opt out or use the [4:19] previous behavior, you can set this back [4:21] to ERB yourself. [4:24] Alongside it, we are shipping the new [4:26] version of Herbs 0.11, [4:28] which is the biggest release so far and [4:30] it ships a lot of features we kind of [4:31] talked about today, but I want to [4:33] highlight a few of those including the [4:34] LSP features, [4:37] which now helps you to find references [4:39] for your partials to see where they are [4:40] being used. So, if you or your agents [4:43] are refactoring your views, they can be [4:45] more accurate and more [4:47] um instance. [4:49] We have first space support if you have [4:51] multiple app uh a mono repo. [4:54] Previews, we have code actions like [4:55] extracting to partials, and these more [4:58] and new powerful cross-file diagnostics. [5:01] A new thing we are shipping as well as [5:02] runtime instrumentation, which takes all [5:05] of your views and looks at every ERB tag [5:08] and instruments them. [5:10] And we can now pair them with the [5:11] existing active support notifications [5:13] and we can [5:15] annotate these tags with the existing um [5:19] no um notifications. [5:21] So, this case, we can subscribe to the [5:23] active record ones [5:25] and for the queries and then match them [5:27] back to the ERB tags. [5:29] And these then get turned to the runtime [5:30] diagnostics, which we now can show in [5:32] the browser [5:34] including the linter um warnings and [5:36] everything else you can see. So, you can [5:38] see the diagnostics, the stack trace, [5:40] how it was rendered. [5:42] And if you hover over any of these stack [5:43] traces, it shows you where on the page [5:45] it actually showed up. [5:49] This also reworks the [5:51] the error screen. So, whenever you have [5:52] an error or it's hot reloading and shows [5:55] you an error, you can see this like [5:56] this. [5:57] So, you know what's what's going wrong. [6:00] And we can also take these diagnostics [6:03] and show this show them as inline hints [6:05] in your editor. [6:07] So, the same diagnostic [6:09] we were showing earlier from the L [6:11] sequel, we can show next to the actual [6:13] ERB target the templates and show them [6:16] where they are coming from. [6:18] You can also hover them to show which [6:20] query you actually ran. [6:22] And this is the same information you can [6:23] actually see in the dev tools in the [6:26] browser as well. [6:28] One more fun thing was that we also [6:30] looked at all the translations. [6:32] So, whenever you have them, you can [6:34] overlay them over the existing code and [6:36] when you put your cursor on the actual [6:37] line, you can see the source behind it. [6:42] There's also a total of [6:43] over 160 new lint rules, which helps and [6:47] you and your agents write more and safe [6:49] view codes, [6:51] which now makes up over 160 lint rules [6:53] to make [6:55] your views more stain sound. [6:58] I want to give a big thanks to all the [7:00] new contributors for this release. This [7:01] is the biggest and most we have seen so [7:03] far. [7:04] And all the 31 that have been [7:06] contributing to this release release [7:07] specifically. [7:10] If you want to read more about this, [7:11] there's going to be a blog post on the [7:12] Heroku blog about all the new features [7:14] that are shipping Heroku 7.1. [7:17] So now, what does this enable? [7:20] Because the Heroku foundation's now in [7:22] all apps and all new apps and all the [7:24] existing apps that are operating, we [7:26] have the compiler as a platform, [7:29] which means you can build more things on [7:30] top of it. [7:32] And Rails famously has been the [7:33] one-person framework, [7:35] which with Rails 7 we got the whole [7:37] simplification of Hotwire and this whole [7:39] HTML over wire approach. [7:41] And with Rails 8 we got the [7:42] simplification of the salt paper and the [7:44] whole sequel light. [7:47] But the one thing I felt was that the [7:49] view layer itself doesn't scale the same [7:50] way as the rest of the framework. [7:54] So, I was asking myself what becomes [7:56] possible when the whole engine, the [7:58] runtime, and everything else understands [8:00] the HTML in your views, too. [8:03] And how can we evolve Hotwire into this [8:05] competitive [8:07] And how can we evolve Hotwire to build [8:09] more competitive and more modern and [8:10] ambitious UIs? [8:12] And then finally, what it takes to [8:14] complete this Hotwire story now. [8:17] And in my opinion, what we were missing [8:18] is this idea of reactivity, or more [8:20] specifically, the fine-grained [8:22] reactivity. [8:25] The design goal for Herb itself was that [8:28] you can take any of the existing ERB [8:29] views, [8:31] and they should be backwards compatible [8:32] with all the tooling. And I was asking [8:34] myself if we can do the same thing for [8:37] the templating the [8:39] reactivity in the interactive [8:40] reactivity. So, you can write more [8:41] interactive UIs. [8:44] And I wanted to explore if there is any [8:47] of these ideas we have from Stimulus [8:48] Reflex over 6 years ago, and if we can [8:51] bring them back into this idea or in [8:52] this mix. [8:54] So, I was looking at all the existing [8:56] frameworks that we had, including [8:58] Stimulus Reflex, Hotwire, Stimulus and [9:00] Turbo, [9:02] Phoenix LiveView, which does very [9:04] similar things, Livewire in PHP, [9:06] Inertia, and the whole kind of set of [9:08] SPAs um from JavaScript. [9:12] And that's the idea of Reaction View. We [9:14] wanted to take a look [9:16] how we can take Action View [9:18] and try to reimagine some of these [9:19] things, [9:20] how we may be able to re-engineer some [9:22] of the things if we need to, [9:24] and finally, to make it actually [9:25] reactive. [9:28] So, today, I'm really happy to show that [9:30] we have been working on this Reaction [9:31] release [9:33] to ship this new big release including [9:35] reactivity. [9:37] And I want you to take a brief look at [9:39] this clip here. [9:44] >> [music] [9:50] [music] [9:55] [music] [10:02] [music] [10:07] [music] [10:17] [music] [10:23] [music] [10:28] [music] [10:36] [applause] [10:42] >> Thank you. [10:43] So I'm happy to release Reaction View [10:45] 0.6 today, which makes all your existing [10:49] views reactive by default. So [10:52] it's reactivity for the HTML you already [10:54] have. [10:56] With the idea that everything a [10:57] client-side framework does, [10:59] you can now get without having to adopt [11:01] one. [11:03] And the best thing is [11:05] you actually already wrote a Reaction [11:06] View template already. [11:09] Because as I said, [11:11] all of your existing views are now [11:12] reactive, and that just works out of the [11:14] box. [11:16] By default, there are in the several [11:18] modes, so you just have the basic [11:20] reactivity, but you have the option to [11:23] go further and add more interactivity to [11:25] it. So it's kind of gradual how you want [11:27] to adopt it. [11:29] If you've been following along from my [11:30] previous talks, I talked about these six [11:32] adoption levels of Reaction View. [11:34] And now we are shipping level four and [11:36] level five. We have been working on [11:39] these external component level six, too, [11:40] but [11:41] they need some more time. [11:44] So, we got the reactive templates now [11:45] and also the option and the possibility [11:47] to render them client-side as well. [11:51] And this all works to this new idea of [11:53] what I call slots. [11:55] >> [snorts] [11:55] >> So, the idea is we take our ERB view, we [11:57] look at all the ERB tags, [11:59] and all of the ERB tags that are in our [12:01] view are now a slot. So, this has three [12:03] slots here. [12:05] And we can also see that we have a loop. [12:08] So, this is now a collection where [12:10] things can go into. [12:12] And because we understand all of the [12:14] whole view, we can know that this is a [12:15] collection and and what we're doing in [12:16] the view. [12:18] So, it understands the full view and [12:20] what you're doing. [12:23] And with this, we can compile our [12:24] templates now into two formats. [12:28] One is for the regular format when you [12:29] render the first page first time, you [12:32] have HTML with a wire, [12:33] and the second one is for any subsequent [12:35] requests where we just sent that or sent [12:38] down the new values that are dynamic. [12:41] So, if you take a look at this here, [12:42] we have this very simple template with a [12:44] title. [12:45] If you pass the title and render this, [12:48] you get this in the browser, and this is [12:50] what the browser sees. [12:52] But the browser cannot tell anymore that [12:53] this part of the template came from this [12:55] one specific ERB tag. This is like [12:57] detached. [12:59] So, what we can do now is we can insert [13:01] HTML comments to split up this [13:04] expression, so that this part here is [13:07] still addressable, [13:09] and that we can specifically update them [13:11] um without having to [13:13] do a full page reload and a full [13:15] morphing operation. So, we can [13:17] So, slots give us a way to address any [13:20] ERB tag and just update them [13:22] specifically. [13:24] And if you wrap this in a [13:26] tag for the region, we also know where [13:28] this came from for which partial, which [13:29] file, so we can address this address [13:32] this uh specifically. [13:34] And if there's a new value, we can just [13:36] update that, and that's how it works. [13:39] So, both the server and the client have [13:41] a map of where the dynamic values go, [13:44] and they also know how to update them. [13:47] And it doesn't matter where the new [13:48] values are coming from, so you can send [13:50] them over HTTP, over action cable, [13:53] over service and events, or even from [13:54] client side itself, it can update and [13:56] reflect these changes without having to [13:58] re-render them on the server side. [14:01] We have this idea of outputs, slots, the [14:05] collections, and conditionals, [14:07] and that's kind of all we need to make [14:08] this all work. [14:10] And as I mentioned, there's no full page [14:12] reloads, there's no DOM morphing, [14:14] it's just these efficient updates that [14:16] know exactly where these values go. [14:19] So, let's take a look here. [14:21] We have a simple template, just [14:22] time.now, [14:24] and we have the output at the bottom. [14:27] So, what we can do now is we can add a [14:28] new button here to say, I want to have a [14:31] refresh, and you can see at the bottom [14:32] it just did the whole live reload in the [14:35] browser. [14:37] So, we have a new data hub action, which [14:38] is similar to stimulus's hub back uh [14:41] data action, [14:42] and we can just tell it to whenever we [14:43] click this, you want to refresh the [14:44] page. [14:46] And this is all you do, this is all you [14:47] have to wire up. [14:49] So, if you click the button, it will [14:50] refresh the page, [14:52] and it will show you in the new dev [14:53] tools [14:54] which of the slots updated and how they [14:56] reflect the changes. So, [14:58] that's kind of all you need to wire up. [15:02] Behind the scenes, it does a get request [15:04] to the same URL you are currently on, [15:06] and it sends this accept header with it. [15:09] And our action in the controller is just [15:12] pretty much empty because there's [15:13] nothing that we are doing there, right? [15:15] Rails implicitly responds to this using [15:18] the format for HTML, [15:20] and reaction view adds this format [15:22] implicitly for the slots. [15:25] So, the first request is still HTML and [15:27] any subsequent requests with this header [15:29] set is going to compile and render [15:31] through the slots format. [15:33] And the slots format itself is pretty [15:35] much just a representation of our form [15:38] schema [15:39] of this exact template. So, we can see [15:42] this template has five slots. So, this [15:43] is going to return five slots, too. [15:46] And because the client knows about the [15:47] schema, how how this works, it can then [15:50] take these values from down here from [15:51] slots number one or zero [15:54] and then update them on the client side. [15:57] And that's kind of cool. That's kind of [15:59] how this works for just the basic [16:00] functionality, but if you want to go a [16:01] step further, make it even more dynamic, [16:03] you want to have UI state, too. [16:05] So, UI state just means that you have [16:07] some kind of variable [16:09] that it doesn't go to the database, [16:10] doesn't have to be persistent, but you [16:11] just need them to show and hide things [16:14] or update things in the UI. [16:17] Because I believe that not every UI [16:18] interaction itself needs to go and do a [16:21] server round trip. [16:23] And typically, you would kind of use [16:24] Stimulus for this to just write a [16:25] Stimulus controller and wire this all [16:26] up. [16:28] But, what if you could just write ERB [16:30] instead? [16:32] So, if I would write this template here, [16:34] I would have this show [16:36] um predicate methods with this if [16:38] condition and say whenever this is true, [16:40] I want to render this um output. [16:43] So, we have this new directive now for [16:46] Herb state, which works the same way as [16:47] a strict local. In this case, we are [16:49] defining a show state with the default [16:52] value of false. [16:54] And because the compiler knows now this [16:56] is a state, [16:58] it has this show down there, [17:01] it just knows what to do. So, we can say [17:03] when we click this button, [17:05] using the data Herb set, we want to set [17:06] the state of show to true. [17:10] And this works the same way as Stimulus, [17:12] so we can also leave out this click [17:13] because it's a button, so it implicitly [17:15] thinks it's a click. [17:18] And with that, we pretty much got this [17:20] all wired up to say whenever we click [17:22] this button now, [17:23] it will show this conditional. [17:26] And this is all in the clients without [17:28] having to a server request. [17:32] Because this is a boolean [17:33] um state, we can also have the state of [17:35] our toggle, so we can toggle it back and [17:36] forth, and that's kind of what you have [17:38] to do. [17:40] There are a few more attributes you can [17:42] use to control these states the state. [17:45] And that's for that. [17:48] But now this is only so much you can do [17:50] with that, so we want to also kind of [17:52] have more interactive UIs using two-way [17:54] bindings. [17:56] And the idea here is that we have [17:58] inputs, we have HTML already. We know [18:00] how to parse and process this because we [18:01] have all the tools. [18:03] So, I was thinking [18:05] if you have this inputs, we know it's a [18:06] value, and if the sole value of this [18:09] attributes is just the ERB expression, [18:12] why can we just not bind it in the [18:14] compile step? [18:16] So, if you define message as a herb [18:18] state in this case as an empty string, [18:20] this [18:21] message state is now bound to the value [18:23] of this input. [18:26] That's how we get a two-way binding. [18:28] So, if you now go and then update your [18:30] view and add even more [18:32] kind of dependent data on it, you can [18:34] type into the fields, [18:36] and it will update accordingly now [18:38] knowing that the message length depends [18:40] on this state value, [18:42] and whenever we type in the field, it [18:43] updates the state and reflects all the [18:44] changes. [18:45] So, you just write declarative markup, [18:47] and it knows how to reflect the changes. [18:51] As I mentioned, there's no request here. [18:54] There's no custom JavaScript. [18:56] It's just a pure understanding of this [18:58] template and then these efficient [19:00] updates. [19:02] We also don't need any Ruby to [19:03] JavaScript translation for this. [19:06] And we also don't need Ruby and [19:08] WebAssembly in the browser to execute [19:10] the the Ruby. [19:12] Because Reaction view sees all the [19:14] methods and all the herb states [19:16] and can analyze them. [19:19] And this compiler knows about the subset [19:20] of Ruby from the typical kind of [19:22] operations you do in views [19:24] and can bake this right into the [19:25] activity model and wire it up [19:28] right there in the compile step. [19:32] So that way [19:33] we can run all of this in the client [19:35] without a server round trip. [19:38] And for everything the compiler does [19:40] understand because it's too complex or [19:42] because it requires a [19:44] a SQL query or some database or [19:46] something else on the server side, [19:49] we can do a network request as a [19:50] fallback. [19:52] So let's take this example here. We have [19:54] a state for Q, it's a search. [19:57] And then down there you see the topic [19:58] search and we have Q in there. [20:01] So the compiler sees we have this Q [20:03] state. It knows this ERB tag with the [20:05] topic search is dependent on this Q [20:07] state. [20:08] So whenever Q's changes, it can update [20:11] and reflect the changes and go fetch [20:12] from the server side, [20:14] get the slots format back and then [20:16] reflect the changes in the in the front [20:17] end. [20:19] And if you look at this, that's just how [20:21] you write ERB. I've been writing ERB the [20:23] whole time, but it's just reactive now, [20:25] which is super cool. [20:29] So the compiler [20:31] knows about all of this. It prepares [20:32] this the the view for the clients and it [20:35] has the counterpart on the server side. [20:37] So whenever a state changes, it knows if [20:39] it can do it all client side or if it [20:41] has to reach back for the server to get [20:43] the new values. [20:46] And if the client actually has to [20:48] fetch from the server, it the server [20:50] knows exactly what could be coming from [20:53] the client so because it already saw all [20:54] the templates. [20:56] And then the client knows how to reflect [20:58] the changes because it also knows about [21:00] the schema, what slots are there and [21:02] what values go where. [21:05] There's no IDs you have to match, turbo [21:07] frames, no turbo streams, no new control [21:09] actions, no new routes, just whatever [21:12] you have been doing already. [21:15] Because we have this understanding of [21:16] the view and can use the fine-grained [21:18] reactivity to have these changes [21:20] reflect. [21:22] And then I was thinking this typical [21:24] idea of lazy loading, which we kind of [21:26] do to defer some of the rendering, [21:28] it has similar problems. [21:31] So, let's say we have this kind of [21:33] template [21:34] with a slow query in here. [21:37] But instead of having to write and [21:38] extract this into a separate partial, [21:40] having to expose it on a separate route, [21:43] what if we just wrap this in a async [21:45] tag? [21:46] This is the async tag with a capital A. [21:49] It's not, I guess, typical from what you [21:50] see, but [21:52] from other frameworks, especially in the [21:53] React world, you have a lot of these [21:55] built-in components. [21:57] So, this kind of takes it and makes it [21:59] async, which means [22:01] we want to provide a fallback for the [22:03] first time you render this. So, the [22:05] compiler sees this setup, [22:07] knows there's a skeleton here, [22:09] and whenever you render it for the first [22:11] time, [22:12] it just renders the fallback, which is [22:13] super quick. [22:15] And then, whenever this comes to the [22:17] client, the browser sees this, [22:19] it knows that this is the async block [22:21] and can [22:22] go talk back to the server, fetch the [22:24] values, and then bring in that actual [22:26] new data. [22:29] This also works with a lazy, [22:31] which is the same thing, but also but [22:33] only when it comes into view, into only [22:35] the viewport, it starts fetching, so [22:39] here's a short demo for this. [22:43] You have the page, you see the value [22:45] loading, and when you go scroll all the [22:47] way down, [22:48] it starts fetching and brings in the [22:49] value. [22:51] Just using this declarative state. [22:56] I'm open to feedback about naming this [22:58] because this is just the way I came up [23:00] with, but if you have any more or idea [23:02] about syntax and naming on this, I'm [23:04] open to feedback here. [23:06] And then I wanted to kind of show what's [23:08] possible with this with just ERB. So, I [23:11] built this kind of music player or music [23:13] library. [23:14] Whenever you click on it, it kind of [23:16] shows immediately [23:18] the details about it. It shows the [23:20] skeletons for loading. You can play back [23:22] some of these tracks, pause and play, [23:25] and then close it again. [23:26] And this is like 100 lines of ERB, no [23:28] JavaScript, and that's kind of what [23:30] you're able to build with this now. [23:34] >> [applause] [23:38] [applause] [23:42] >> And then finally, this whole idea about [23:44] optimistic UI, because we have all this [23:46] understanding now, we can push it all [23:48] down to the clients, park our markup [23:50] there, so we can do client-side [23:52] rendering. [23:54] So, here I have a simple app, a chat [23:56] app, which I can send messages into. [23:59] And these messages get rendered on the [24:00] client-side immediately. [24:02] And whenever they might fail, they also [24:04] can render instantly on the client-side, [24:07] so you can retry them and then send them [24:09] to the server. [24:11] You can edit them. This whole [24:12] interaction is also just client-side. [24:13] You click the button, you click delete. [24:15] This doesn't need a round trip because [24:17] it's all just hiding and showing things. [24:20] And then finally, the star from the [24:22] message, the star is happening [24:24] immediately on the client. [24:26] It queues a request in a queue. [24:30] And whenever that request might fail, I [24:32] can roll back to the last state it knows [24:35] was good. [24:36] Because it takes it takes a lock of all [24:38] the state changes that happen, so you [24:40] can kind of go back and forward in state [24:42] changes. [24:46] So, that's kind of to show that I think [24:48] you can do a lot of pure ERB now. Just [24:50] having this precompiled step and this [24:52] compiler knowing about your view, you [24:55] can build these interactive UIs. [24:57] And if you need more, if you want to [24:59] build more even more ambitious UI's, [25:02] there's a JavaScript API you can use to [25:04] interact with the runtime directly. [25:07] Which you might be wanting to use if you [25:08] want to use any NPM packages or [25:11] any more like click events or whatever. [25:14] There's just a low-level JavaScript API [25:16] you can use for that, too. [25:19] All you have to do is add the reaction [25:20] view gem, run the generator, and that's [25:23] what you have to do. That's it. [25:26] There's docs on reactionview.dev and the [25:28] demos I've shown today are on [25:29] demo.reactionview.dev, too. [25:32] Take a look at the code. It's all open [25:33] source, so [25:35] you can see it for yourself. [25:38] So, I want to give a quick outlook of [25:39] where I think this could be heading. [25:42] The one thing we have been doing and [25:43] seeing from people is that there's still [25:45] a struggle with managing CSS and [25:47] JavaScript. [25:48] So, we have now also this style scope [25:51] idea, so you can put a style tag into [25:53] your ERB view. [25:56] And it can scope it just to that one [25:57] specific partial, so the styles don't [26:00] leak. [26:02] We have a new gem for this called [26:03] lightning-css-ruby, which does all the [26:05] pre-processing. It's a tool written in [26:06] Rust, and this has Ruby bindings now. We [26:09] just kind of call out to this, [26:11] do some of the modification, and then [26:13] it's scoped to just this one specific [26:15] page. [26:17] We haven't been working on script scope [26:18] yet, but I think there's also a [26:20] potential where you want to put a script [26:22] scope into your view [26:24] where you have all the scripts just [26:25] apply to that one specific page. [26:28] For that, we have built oxc-ruby, which [26:31] is the tool in Rust that Vit, the [26:34] bundler, uses. [26:36] And Rollup is also something Vit uses [26:38] under the hood to bundle JavaScript, [26:40] which means you can use TypeScript, you [26:43] can use JSX if you really want to. [26:46] This can bundle it without having any [26:48] JavaScript installed, so you don't need [26:49] Node.js, and it's still all compatible [26:51] with input maps. [26:55] And if you can take a look now, I think [26:56] that it's also more opportunities now to [26:58] do an actual proper offline mode because [27:00] we can push all the markup down. [27:03] There might be a possibility to add [27:04] React components to the mix now to go [27:06] side by side with the ERB. [27:10] And I think more importantly, I think [27:12] there has been missing an abstraction [27:13] for components that gems could provide. [27:16] So, if a gem can provide a component [27:19] that has a server client side parts [27:21] that you can just bring them into your [27:22] view layer and the Herb Engine knows how [27:25] to deal with them. [27:29] So, to conclude, [27:31] I think I'm really happy with where this [27:33] direction of Herb and React Native is [27:34] going [27:35] and how these things are shaping up. [27:38] And that we can now pretty much go back [27:40] to write just ERB views and Ruby to have [27:43] these full reactive UIs. [27:46] And if you take a look at the bigger [27:48] vision, I think that this could provide [27:50] a great foundation for the future of the [27:51] Rails front ends [27:53] and that this kind of tries to offer a [27:56] spectrum of solutions for teams [27:59] for however much they want to adopt [28:01] JavaScript in their stack. [28:04] And most importantly, to expand what's [28:06] possible with Hotwire so we can keep the [28:08] simple approach that Rails always kind [28:10] of [28:11] tried to prove and make it even more [28:13] powerful. [28:16] And if the application really needs it, [28:18] that you have a way now to write more [28:20] JavaScript if you really want to. [28:25] But as I mentioned, I just want to also [28:27] provide a way that if you have existing [28:29] React components or want to use them, [28:32] that you have now a way to do this [28:33] without having to abandon this full [28:35] stack idea which makes Rails so special. [28:39] So, that's why I'm really happy that [28:42] Herb made its way into Rails because [28:44] this provides a foundation now for all [28:46] apps to build even more things, powerful [28:50] things on top of it. [28:52] Reaction view has become what I always [28:54] wanted Stimulus Reflex to be in a way. [28:56] But I'm sure that we are just at the [28:58] start and we can build even more things [29:00] if we start to iterate on this. [29:03] But I can't do it all by myself. I need [29:06] inputs, I need more real applications, [29:09] more real adopters that try to push [29:11] what's possible with this. [29:13] And I would love to talk to you if you [29:14] have any ideas, opinions on all the [29:17] things I've shown today. [29:18] Or if you are skeptical and have [29:19] questions about where this is heading. [29:22] So I would love really love your input [29:24] and feedback. [29:25] And finally, I want to give some quick [29:27] acknowledgements [29:29] to all the people that have been [29:30] contributing so far. [29:32] It's a lot of people that have stepped [29:33] in and helped shape this to the way it's [29:36] today. [29:37] And all the people that have been [29:38] reporting issues, opening pull requests, [29:41] and helped out with this. Without all [29:43] these people, it wouldn't be possible [29:45] what we have today. [29:47] And finally, to all the sponsors that [29:49] have been supporting me on this journey. [29:53] Thank you for the continued support. [29:55] Please try the tools, report issues, and [29:57] give feedback. [29:59] Give it a star on GitHub for both Herb [30:00] and Reaction View. [30:03] Check out the new docs with the new [30:05] landing pages on reactionview.dev and [30:08] tools.dev. [30:09] And that's what I have for today. [30:11] >> [applause] [30:16] [applause] [30:17] [music] [30:22] [music]