Hotwire: building responsive Rails apps
Tags: ruby, rails, hotwire, webdev
Published: 2022-08-08 05:48:00


If you're developing modern single page applications with Ruby on Rails, you're most likely using some fancy JS framework for your UI to be updated nicely, without page reloads. And there is really not much you can do without using them, that's kind of a standard these days... until Rails got Hotwire.
With Hotwire you can get fast and responsive web application, but without writing ton of Javascript. Well, sounds great, but what is Hotwire? In this article we'll go through the basics of Hotwire, as well as build a sample app using it.

## What options are there for responsive Rails apps?

On June 25, 2013, Rails 4 was released introducing Turbolinks. What Turbolinks are doing for "responsiveness" of Rails? Turbolinks intercept all link clicks and instead of sending regular `GET` request it sends asynchronous Javascript request (AJAX) to fetch HTML. It then merges the `head` tag of the fetched page and replaces the whole `body` tag of the page, so there is no full page reload. No stylesheets or scripts reloading, which meana a faster page navigation. But it still is replacing the whole `body`, not only changed parts of the page.

But what if you want to reload only parts that changed? Well, you could use Rails AJAX helpers, when you mark some elements as `data-remote='true'`, which makes those element sending AJAX `GET/POST` requests instead of regular `GET/POST` requests. And Rails responds with generated JS code, which then is executed by the browser to dynamically update those parts of the page.

Then we can get to some JS components on the frontend (e.g. using React) to make an app feel even more responsive. So, JS component sends an AJAX request and Rails server responds to it with JSON data. Then frontend framework transforms received JSON to DOM elements and updates DOM to reflect those changes. And it works nice, the only downside is that it's mixing server side rendering and client side rendering on a page.

Another, more traditional way these days, is going all in and using React, Vue or another JS framework on the frontend to use only client-side rendering, i.e. so-called Single Page Application (SPA). With this approach frontend is a separate application which sends AJAX requests to the Rails server, and Rails is being solely a JSON API. As you probably know there's a whole lot of complexity of building, maintaining and deploying two separate apps with interchangeable data.

But what if you could build an SPA w/out all that complexity of building two separate apps and writing lots of Javascript code? Here's what Hotwire can help you with.

## What is Hotwire?

Hotwire is an alternative approach to building SPA like web apps, rendering all of the HTML server side using Rails templates, while keeping the app fast and responsive. Keeping redering on server side makes you development experience simpler and more productive. Hotwire name basically is an abbreviation for `HTML Over the Wire`, which means sending generated HTML instead of JSON from the server to the client. It also does not require you to write much of custom javascript code. Hotwire is made of Turbo and Stimulus.

## What is Turbo?

Turbo gem is the heart of Hotwire. It's a set of technologies to dynamically update the page, which speeds up navigation and form submission by dividing pages into components whiuch can be partially updated utilizing Websockets as a transport. If you ever worked with websockets in Rails, you most likely know that Rails is using ActionCable to handle websockets connection, and it's included in Rails by default. And Trubo gem consists of: Turbo Drive, Turbo Frames and Turbo Streams.

### Turbo Drive

Turbo Drive is used to intercept links clicks (just like Turbolinks were doing it previously) and also intercept form submissions. Turbo Drive then merges the head tag of a page and replaces the body tag of the page. Same case as with Turbolinks, no full page reload, which maybe fast for some pages, but not really as responsive as the 2022 app is expected to be, so you might consider updating only some parts of the pages, not the whole body. Here's where Turboframes come in handy.

### Turbo Frames

You can simply make a part of a page a Turbo Frame, by simply wrapping it in a turbo-frame tag with a unique id
```ruby
<turbo-frame id="13">
  ...
</turbo-frame>
```
This makes any interactions within the frame to be scoped to that frame. Any interaction within that framne send an AJAX request to the Rails server, and server responds with an HTML only for that frame. Which allows Turbo to automatically replace only that frame on a page. And this does not require to write any Javascript.
But what if you want to update multiple parts of the page at the same time? That's what Turbo Streams can help you with.

### Turbo Streams

When user interacts with an element on the page (e.g. form/link) and Turbo Drive sends an AJAX request to the server, the server responds with an HTML, consisting of Turbo Stream elements. And those are like directions for Turbo to follow in order to update affected parts of the page. Turbo Streams include seven available actions: `append, prepend, (insert) before, (insert) after, replace, update, and remove`:
```ruby
<turbo-stream action="append" target="target_a">
  <template>
    HTML
  </template>
</turbo-stream>

<turbo-stream action="prepend" target="target_b">
  <template>
    HTML
  </template>
</turbo-stream>

<turbo-stream action="replace" target="target_c">
  <template>
    HTML
  </template>
</turbo-stream>
```

Turbo Streams are using ActionCable to deliver updates to multiple clients via websockets asynchronously. Again, you're getting all of this w/out writing any of the Javascript code. But even if you need some custom Javascript for any reason (e.g. some animation, datepicker, etc.), Hotwire got you covered here with Stimulus.

## What is Stimulus?

Just like Rails which has its controllers with actions, Stimulus allows you to organize client side code in a similar way. You have a controller, which is a Javascript object, which defines actions, i.e. Javascript functions. Then you connect the controller action to interactive element on a page using HTML attributes. The action is then run in response, when DOM events are triggered.

## Let's create a sample Rails app with Hotwire

Now having read all of the above, you might be wondering: how do I work with it? Hotwire is pretty simple to use, all that we need is a standard Rails app and Redis server.
First you need to have Ruby 3 and Rails 7 and Redis server to be installed, I'm not going to cover installation process of those, but you can easily find any instructions you need based on your platform.

So, let's setup a new Rails app (we'll be using Bootstrap as css option, just to make our app look a bit better):
```
rails new bookstore --css bootstrap
```
After Rails generated all the needed files, cd into the app directory:
```
cd bookstore
```

Rails 7 initial app has everything we need to start using Hotwire, the Gemfile includes: Redis gem, Turbo-rails gem and Stimulus-rails.
Make sure that you have Redis server up and running. Redis is required because it's used by ActionCable to store websockets related information.
The default address and port for Rails to connect to Redis server is set in the `config/cable.yml`

```ruby
development:
  adapter: redis
  url: redis://localhost:6379/1
```

Then we can genertate our model, controller and migration, which will be "Books" in our case of Bookstore. It'll have a string title, description of type text and likes counter as integer:

```ruby
rails g scaffold books title:string description:text likes:integer
```

Let's fix generated migration, so we have 0 likes by default for any book we add to the db:
```ruby
class CreateBooks < ActiveRecord::Migration[7.0]
  def change
    create_table :books do |t|
      t.string :title
      t.text :description
      t.integer :likes, default: 0

      t.timestamps
    end
  end
end
```

Don't forget to create a database for our app, run in terminal:

```ruby
rake db:create db:migrate
```

Lets make books list page the root page of the app, open `config/routes.rb` and add the missing root declaration:

```ruby
Rails.application.routes.draw do
  root 'books#index'
  resources :books
end
```

Then you should be able to run rails server with `rails server` command or `./bin/dev` (which will also watch css and js changes) in your terminal and, when you visit [http://localhost:3000](http://localhost:3000) in your browser, you should see something like this:

![Not responsive books list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0lccyawmlybjac8mwlu3.png)

Let's change the Book partial `app/views/books/_book.html.erb` to this:
```ruby
<%= turbo_stream_from "book_#{book.id}" %>

<%= turbo_frame_tag "book_#{book.id}" do %>
  <div style="background: lightblue; padding: 10px; width: 400px;">
    <h2><%= book.title %></h2>
    <p><%= book.description %></p>
    <br>
    <%= button_to "Like (#{book.likes})", book_path(book, book: { likes: (book.likes + 1) }), method: :put %>
  </div>
  <br/>
<% end %>
```
`turbo_stream_from` tells Hotwire to use websocket for updates of a frame identified with `:book_id`, and `turbo_frame_tag` identifies a frame, which can be replaced with partial on update.

To tell Turbo that we want to add each new created book to the start of the books list, and update likes count on each like button click, we need to add  following callbacks to `app/models/book.rb` file (also let's add a validation):
```ruby
class Book < ApplicationRecord
  after_create_commit { broadcast_prepend_to :books }
  after_update_commit { broadcast_replace_to "book_#{id}" }

  validates :title, :description, presence: true
end
```
First one tells Turbo to use `:books` Turbo Stream for updates on create, and second one tells to use `:book_id` to replace partial with updates.

Then let's fix the ordering in our books controller and also add the new book variable assignment (so we could create a book from the root path) in `app/controllers/books_controller.rb`:
```ruby
...
def index
  @books = Book.order(created_at: :desc)
  @book = Book.new
end
...
```

We should also edit `app/views/books/index.html.erb` to add our Turbo Streams and Turbo Frames:
```ruby
<h1>Books</h1>

<%= turbo_stream_from :books %>

<%= turbo_frame_tag :book_form do %>
  <%= render 'books/form', book: @book %>
<% end %>

<%= turbo_frame_tag :books do %>
  <%= render @books %>
<% end %>
```

To avoid redirects when we create a new book or update an existing one and stay on the same main page, we need to also edit create and update actions in `app/controllers/books_controller.rb`:

```ruby
...
def create
  @book = Book.new(book_params)

  respond_to do |format|
    if @book.save
      format.html { redirect_to root_path }
    else
      format.turbo_stream { render turbo_stream: turbo_stream.replace(@book, partial: 'books/form', locals: { book: @book }) }
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

def update
  respond_to do |format|
    if @book.update(book_params)
      format.html { redirect_to root_path }
    else
      format.html { render :edit, status: :unprocessable_entity }
    end
  end
end
```

At this point, our bookstore app should look like this:


![Responsive Books List](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wrp11ohsnwu8dbkf878y.png)

Any time you create a new book using the form on the main page, Turbo prepends it to the books list, w/out page reloads. If you open multiple tabs in the browser - it will update all of them. "Like" buttons are also working w/out page reloads and update likes counts for the book in all of the tabs. And all of this with not a single line of Javascript code. How cool is that?

## Conclusion

This sample application is just a basic example of what you can do with Hotwire in Rails, but you can do much more complex things with Turbo and Stimulus.
So if you want to start a new SPA with Rails, think twice if you need React, Vue or any other framework on the frontend, it might be much more productive to give Hotwire a try. There's a big chance it will make you happy.