For years, web development felt like a binary choice: build a traditional server-side application and accept “clunky” page reloads, or adopt a complex Single Page Application (SPA) architecture using React or Vue to get a snappy user experience.
For Rails teams, the middle ground often meant combining the two, i.e. running a Rails API backend with a full JavaScript frontend. But this hybrid approach often backfires. Instead of getting the best of both worlds, teams end up fighting the complexity of both.
The Cost of the Hybrid Stack
Consider the gauntlet of Rails intertwined with React.

- Ruby and Rails already requires the team to master two forms of art. The addition of React doubles the demand to encompass Ruby, Rails, JavaScript, and React.
- Likewise, tooling—dependency management, configuration, builds and deployment— doubles too as the frameworks do not share libraries.
- Testing multiplies, too, but the ardor is not the number of tests. Instead, the polyglot approach divides the application’s test suite into two ecosystems. Rails might use RSpec. React requires Cypress or Playwright.
Recognizing the cumulative hurdles posed by heterogeneous frameworks and languages, teams have sought to coalesce and restrict development to one language. For example, React has expanded to both client and server to craft reactive applications solely in JavaScript.
The Rails-Native Alternative
Recognizing the cumulative hurdles posed by heterogeneous frameworks and languages, teams have sought to coalesce and restrict development to one language. For example, React has expanded to both client and server to craft reactive applications solely in JavaScript.

But Ruby developers have also innovated tools to encompass and enhance client and server development. Three emergent and powerful tools merit attention:
- ViewComponent adds object-oriented paradigms to Rails views. As the authors state, “[ViewComponent is a] framework for creating reusable, testable & encapsulated [user interface elements]” based on Ruby classes.
- Hotwire sends HTML via the Fetch API and WebSockets, providing refresh-free, dynamic content updates.
- Stimulus infuses an iota of JavaScript for control. As with other Rails technologies, the tenet of convention over configuration makes adoption a snap.
Rails now ships Turbo and Stimulus by default. ViewComponent isn’t part of Rails core, but integrates seamlessly with Rails. All three are easy to set up in a new or existing Rails app.
Rails developers should consider ViewComponent, Hotwire, and Stimulus before reaching for React or Vue. Building modern web applications with the trio:
- Consolidates the logic to render an element of the application into a single Ruby class. This mirrors the intent of a React component.
- Enhances the interactivity and responsiveness of a Rails application without the addition of a JavaScript framework and its attendant tooling and thrash.
- Scales fluidly, from a single component, to a portion of a page, to an entire facet of an application, or to all features.
An all-Ruby stack also keeps the majority of the code base in Ruby and allows Rails developers to contribute expertise to front- and backend development. A homogeneous technical stack speeds development.
This article presents an introduction to ViewComponent, Hotwire, and Stimulus. It presents the strengths and advantages of each and recommends how teams can adopt all three incrementally. For brevity, code is limited. See the embedded links for documentation and examples for each project. Finally, the article presents the Island Architecture, a strategy to add the minimum of JavaScript when it’s absolutely needed.
ViewComponent: An Object-Oriented View
Rails implements the Model-View-Controller design pattern (or MVC) in Ruby. MVC divides the logic of an application into three concerns:
- Models represent items in the system.
- Views present information, usually in the form of a web page or an API response. Each view is composed of a layout and one or many templates and partials. (You can think of a layout as an HTML page wrapper, including CSS, assets, page headers and other decorative elements and metadata. A template usually captures some action, such as displaying the specifics of a cart. A template is composed of partials, where each partial can be thought of as a section of a page or even a component.)
- Controllers transit information (user input, API calls) to facilitate change.
An Order is a common model. A checkout form is a common view. A SessionController, to log in a user, is an oft-used controller.
While Rails’s layouts, templates, and partials are easy to use, the technology nonetheless has many disadvantages:
- There is no definitive programmatic interface to each view. Each layout, template, and partial is essentially a black box: The local and instance variables each view depends on can be discovered only by inspection of the source code of the view (and likely the associated controller).
- The data structures required to render any given view are likewise obfuscated. The plurality of a variable name might provide a clue to its structure—@users implies a list—but is the list a collection of ActiveRecord objects, hashes, or some other set of attributes? Again, scrutiny is required to discover the assumptions of a view.
- Typically, a Rails view is not easily tested in isolation. A system test can be effective, but it’s also overly expansive and can require extensive data scaffolding and coding to drive scenarios. Ideally, a Rails view could be tested as a whole.
ViewComponent is a library to author a Rails view as a unit, akin to a Plain Old Ruby Object, including an initializer and public and private methods. Further, since a ViewComponent is simply a Ruby class (subclassed from ViewComponent::Base), it can be composed from concerns and tested separately like any other unit. ViewComponent also provides its own test helpers to assert correctness of rendered content.
Here is a sample ViewComponent—with some special shortcuts—taken from the official documentation:
1class Catalog::ProductComponent < ViewComponent::Base
2with_collection_parameter :item
3
4erb_template <<-ERB
5<li>
6<h2><%= @item.name %></h2>
7<span><%= @notice %></span>
8</li>
9ERB
10def initialize(item:, notice:)
11@item = item
12@notice = notice
13end
14endIt is rendered from a Rails app via a single line of code:
1<%= render(
2 Catalog::ProductComponent.with_collection(@products, notice: "hi")
3)%>This code produces a collection of items as a series of <li> elements. Here is how it works:
- The namespace Catalog indicates it’s useful for rendering items for sale.
- with_collection_parameter :item configures the component to iterate automatically over its first argument in variable @products. Each iteration calls the initializer with the parameter :item and all remaining arguments passed to with_collection.
- erb_template provides an inline partial. This template is rendered once for each item. It renders each item’s name and the notice “hi” for each item.
Like other Rails views, render produces the output of a component. Further, there are no loops in the ERB and the component can be tested in isolation using any combination of products. The use of an inline partial is not required but it mimics how a React or Vue component internalizes data and layout, a nicety for maintenance and conceptualization.
This ViewComponent can be embedded in another ViewComponent to produce a catalog page. Or it could be added to a cart to display products during checkout. It’s entirely modular, reusable and contained within a simple class.
Hotwire and Stimulus: Minimal JavaScript for Reactive Applications
While ViewComponent simplifies the V in MVC, let’s next examine how Hotwire and Stimulus power interactivity and responsiveness in Rails applications.
While sometimes referred to interchangeably and usually combined in working code, Hotwire and Stimulus are actually two independent technologies, each with a unique purpose.
- The core of Hotwire is Turbo, a set of techniques that speeds up page changes and form submissions without writing custom JavaScript. Installed via the turbo-rails gem, it offers three distinct tools:
- Turbo Drive accelerates links and form submissions by suppressing full page reloads.
- Turbo Frames decompose pages into independent contexts, allowing you to lazily load or update specific areas.
- Turbo Streams deliver real-time page changes over WebSocket connections or in response to form submissions. Together, Turbo Rails features allow developers to build high-fidelity reactive web applications that rival SPAs.
- Stimulus is a tiny JavaScript framework to monitor page elements, sprinkle in logic, and orchestrate updates. It renders nothing, but simply connects code to content and reacts to changes. A Stimulus controller (written in JavaScript) monitors one or more elements for events. An event causes an action—a method within a controller—and the action permutes one or more values within the element.
Here’s a very brief example from the Stimulus documentation. First is HTML with annotations to elements specific to Stimulus:
1<div data-controller="clipboard">
2PIN: <input data-clipboard-target="source" type="text" value="3737"
3 readonly>
4 <button data-action="clipboard#copy">Copy to Clipboard</button>
5</div>Next, the JavaScript for the Stimulus controller, as captured in the file app/javascript/controllers/clipboard_controller.js:
1//app/javascript/controllers/clipboard_controller.js
2
3import { Controller } from "@hotwired/stimulus"
4
5export default class extends Controller {
6static targets = [ "source" ]
7copy() {
8navigator.clipboard.writeText(this.sourceTarget.value)
9 }
10}Convention, as is the way in Rails, connects the HTML content to the JavaScript code.
- The data-controller attribute of the div names the Stimulus controller to affect the element. Its name is clipboard. In turn, Stimulus locates the controller by looking for a file named app/javascript/controllers/clipboard_controller.js.
- The attribute data-clipboard-target maps the input element to the Stimulus controller named clipboard and its value to one of the targets named source. Put another way, this HTML specifies that a change to the input value is immediately reflected as this.sourceTarget.value in the controller.
- Finally, the data-action attribute on the button element maps a click to the controller’s copy() method.
Hence, when the button is clicked, the current value of the input element is copied to the browser’s clipboard where it can be pasted to any other application.
Hotwire renders one or more portions of a page. Stimulus monitors elements of the page. Combined, the two can replace a typical single-page web application framework.
State is captured in the HTML, not the JavaScript controllers. The server remains the source of truth. Indeed, all the techniques for caching in Rails—fragment caching and Russian doll caching— remain viable and can be used with Hotwire to provide optimal application performance.
The only difference using Hotwire is how much HTML must be delivered to the browser as a response.
The Vue Island: Injecting Minimal, Purposeful Vue into Rails
As capable as ViewComponent, Hotwire, and Stimulus are, there are occasions when an application requires React or Vue. For example, an application might benefit from or require a special user interface component such as a date picker or graph. Adding interactivity to an otherwise static web page is another sound reason for such integration.
The Islands Architecture is an approach to add limited code and componentry from a JavaScript framework into a page. Rather than adopt an entire framework, a small island of interaction is created within a larger sea of other HTML content. The pattern minimizes code size, client-side loading, and limits complexity to specific, perhaps strategic uses. In some cases, loading can be delayed until the island becomes visible to the user.
Vue can be intermingled with Rails projects using either Rails’s own import maps or the vite_rails gem. Both compile and deliver Vue components to the browser.
If you already use import maps, embedding a Vue island in a Rails view requires just a few steps:
- Use the rails command-line tool to install Vue via import maps.
- Create the Vue component within the structure of your Rails application. app/JavaScript/components is suitable.
- Create the Rails view with a div element with a unique ID to serve as the mount point for your Vue application.
- Create a small JavaScript file to import Vue and your Vue component and mount the component within the Rails view.
- Run the Rails server.
When the page loads in the browser, the named div is amended with the Vue component.
An Incremental Approach to Adoption
A significant advantage of ViewComponent, Hotwire, and Stimulus is compactness. Each technology can be incorporated strategically, judiciously, and even minimally to any existing or new Rails application.
Embrace any of the technologies to construct a brand-new feature; adapt a single page or view; or introduce a new component to invigorate a user interface. A Vue island is yet another technique to inject a Vue component into an otherwise standard Rails view.
Better yet, the tools discussed here can be used solely or in combination. Building incrementally with these technologies avoids the ardor, delay, and expense of a rewrite. If you have a team of Ruby experts, these (relatively) new technologies let each developer in the team contribute to front- and backend development.
Smaller teams can realize an outsize benefit from unification around Ruby as specialization in React or Vue is unnecessary.
Here’s a series of assessments to help plan and smooth adoption:
- If a new project is emerging, use it to pilot adoption and develop competency and experience. If someone within the organization has experience with ViewComponent, Hotwire, and Stimulus, perhaps partner with the developer to rationalize adoption and validate assumptions and early implementations.
- Watch for opportunities to introduce the tech into an existing code base. Is there a new feature where novel techniques would hasten or simplify development?
- Find areas where code could be refactored and improved with ViewComponent, Hotwire, and/or Stimulus. If the code already uses a framework, discover its extent and whether it can be deprecated and removed over time in favor of a more Ruby-centric stack.
- Place your ViewComponent classes in app/components to mirror how Rails organizes other related classes such as models and helpers in app/models and app/helpers, respectively.
- Use namespaces to further categorize ViewComponent classes. For example, the subdirectory app/components/UI might contain individual user interface elements such as a button or carousel. A class name for the latter is UI::Carousel. Namespaces aid developers in navigating a code base.
- A ViewComponent need not be limited to the content of a web page. You can also use a ViewComponent for email messages. Better yet, ViewComponent also supports component previews.
- Build a unit test for each ViewComponent. ViewComponent tests are typically blazing fast because there are no database requests required. Fake objects can stand in for Rails models.
- Consider Hotwire and Stimulus training for the team. Several (inexpensive) guided courses are available online. If your developer prefer reading over video instruction, several books focus on the topics.


