Using Hotwire with Rails

If you are looking for a way to speed up page changes and form submissions and divide complex pages into components without writing any JavaScript code, this post will help you take Rails to the next level with Hotwire. This article will teach you how to use a tool for server-side rendering. What is Hotwire? […]

If you are looking for a way to speed up page changes and form submissions and divide complex pages into components without writing any JavaScript code, this post will help you take Rails to the next level with Hotwire. This article will teach you how to use a tool for server-side rendering.

What is Hotwire?

Hotwire provides a way to build modern web applications without writing JavaScript by sending HTML instead of JSON over the wire, which makes pages load faster. It maintains rendering on the server-side and allows for a simpler and more productive development experience, as Rails has always incorporated into its technologies, without sacrificing the speed or responsiveness associated with a traditional single-page application (SPA).

The core of Hotwire is the Turbo gem. It is a set of complementary techniques that speed up the navigation of pages and submission of forms, dividing complex pages into components and transmitting partial updates of the pages through the WebSocket (which consists of ActionCable, channels, and streaming data).

Why should you use it?

If you struggle with JavaScript and want to take advantage of a better user experience through the feeling of navigating between pages much faster, Hotwire is an alternative for you.

How does Hotwire work? Hotwire uses server-side rendering (SSR) to solve some of the problems associated with SPAs while maintaining their main advantages. SSR reverses the rendering process, bringing part of the SPA rendering effort to the server, similar to traditional loading. SSR can provide users with more efficient loading of the application since some of the rendering is done on the server. In addition to the possibility of improving performance, it helps deal with some SEO problems, such as indexing.

Is simple to use?

It’s very simple to use; you just need the default package of a Rails project (Ruby, RoR, ActionCable, and WebSocket), a Turbo gem that downloads all the JS dependencies, and Redis to store some temporary data while browsing the WebSocket.

You don't need to learn another language (JS) to have the speed of a single-page web application because Turbo is complemented by Turbo Drive, Frames, Streams, and Native. Drive speeds-up links and forms and reduces the need to reload networks, while Frames divides networks into independent contexts that are easier to load.

How is it used with Rails?

This section will show you a step-by-step example.

For this example, we will need:

  • Ruby
  • Ruby on Rails
  • Redis
  • SQLite (default database)
  • Gem Hotwire
  • TurboRails
  • StimulusJS
  • WebSocket
  • ActionCable

What project will we create?

We will create a social media project.

First, open your terminal. Let's begin with a new rails project:

rails new social-media

Enter your project:

cd social-media

Adding Hotwire

Add the Hotwire gem to your project:

bundle add hotwire-rails

Alternatively, open your Gemfile and add this:

gem "hotwire-rails"

If you use the second option, you’ll need to run bundle now.

bundle install

Then, you’ll install it.

rails hotwire:install

Initial settings

Now is a great time to analyze the initial settings that Hotwire made. When visiting the Gemfile, it should look like this:

gem 'redis', '~> 4.0'

Why do we need Redis?

The Redis gem was added because ActionCable needs it to store some temporary data while browsing the WebSocket. However, just installing Redis is not enough to use it. We need to check whether it is configured correctly. When you go to the config/cable.yml file, it should look like this:

development:
 adapter: redis
 redis://localhost:6379/1

Make sure Redis is running when your start your application (redis-server).

JS dependencies

Check the dependencies on package.json:

dependencies: {
 @hotwired/turbo-rails: ^7.0.0-beta.5,
 @rails/actioncable: ^6.0.0,
 @rails/activestorage: ^6.0.0,
 @rails/ujs: ^6.0.0,
 @rails/webpacker: 4.3.0,
 stimulus: ^2.0.0
}

Generating our model

After checking all the files, we will generate our views, controllers, models and migrations for the posts table with the body, and likes columns. To do this, run the following in the terminal:

rails g scaffold posts body:text likes:integer

Creating our database

Now that we have everything generated, we need to send these changes to the database, so run the following in the terminal:

rails db:create db:migrate

If everything goes well, we can run the server (rails server) and check whether everything is OK. To do so, we will run the server and then visit the posts page, which will be http://localhost:3000/posts:

posts
printscreen of the posts directory

Listing the posts

Now, we will list the posts. To do so, we will create the app/views/posts/_post.html.erb file and add the following code to it:

<div style="background: lightgrey; width: 300px; padding: 10px;">
  <%= post.body %>
  <br>
  <%= link_to :edit, edit_post_path(post) %>
  <%= button_to "likes (#{post.likes || 0})", post_path(post, like: true), method: :put %>
</div>
<br>

Validations

We need to validate the body field (which cannot be null), and we will tell the broadcast to display a tweet on the same screen as the first tweet after creating it. To do this, we will edit the file app/models/post.rb and insert the following code:

class Post < ApplicationRecord
  validates_presence_of :body

  after_create_commit { broadcast_prepend_to :posts }
end

Ordering

We have to order our posts, so open your controller (app/controllers/posts_controller.rb).

...
def index
  @posts = Post.all.order(created_at: :desc)
  @post = Post.new
end
...

Wrapping up

Now, let's edit our index (app/views/posts/index.html.erb) to show the new post page and the list of all posts.

<%= turbo_stream_from :posts %>

<%= turbo_frame_tag :post_form do %>
  <%= render 'posts/form', post: @post %>
<% end %>

<%= turbo_frame_tag :posts do %>
  <%= render @posts %>
<% end %>

Redirecting to the index

Finally, let's modify the create method in our controller. To avoid redirecting us to show post page, let's keep everything on the same page.

...
def create
  @post = Post.new(post_params)

  respond_to do |format|
  if @post.save
   format.html { redirect_to posts_path }
   format.json { render :show, status: :created, location: @post }
 else
    format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) }
    format.html { render :new, status: :unprocessable_entity }
   format.json { render json: @post.errors, status: :unprocessable_entity }
 end
  end
end
...

Our final page should look like this:

final-page
printscreen of the final page

Checking the logs

When a post is created, check your terminal logs; they should look like this:

[ActionCable] Broadcasting to posts: "<turbo-stream action="prepend" target="posts"><template><div style="background: lightgrey; width: 300px; padding: 10px;">n  first postn  <br>n  <a href="/posts/1/edit">edit</a>n  <form class="button_to" method="post" action="/posts/4?like=true"><input type="hidden" name="_method" value="put" /><input type="submit" value="likes (0)" /><input type="hidden" name="authenticity_token" value="<token>==" /></form>n</div>n<br>n</template></turbo-stream>"

Thoughts

It means that ActionCable is working with turbo stream with a super fast performance, as you can see:
Completed 302 Found in 18ms.

Deploying

Do we need another executable to run alongside our Rails server?

No, because ActionCable works fine with popular servers, such as Unicorn, Puma, and Passenger.

Does Heroku support Hotwire?

Yes, Heroku supports the base of Hotwire (websockets). If you have any further questions,
you can check Heroku’s official documentation.

In case you are wondering why we are using Redis, there's one more reason

If Heroku is used without Redis, messages will not be sent to everyone when your app is scaled to more than a single dyno.
Due to the stateless nature of the current app, clients connected to the first dyno won’t get messages
sent by clients connected to the second dyno. Redis solves this problem by storing the message state in global storage.

Set up Redis on Heroku

To establish a Redis CLI session with your remote Redis instance, use heroku redis:cli.
If you don't specify an instance, then the instance located at REDIS_URL is used by default.
If you have more than one instance, specify the instance to which you want to connect.

How does it work on the application server?

We need to set up each dyno to use the Redis pub-sub system.
All the dynos will subscribe to the same channel (the default) and wait for messages.
When each server gets a message, it can publish it to the connected clients.
We’ll cover subscribe in a separate thread, as subscribe is a blocking function,
so waiting for a messages will stop the execution flow. In addition, a second Redis connection is needed since once a subscribe command has been made on a connection, the connection can only
unsubscribe or receive messages. Learn more about scaling here.

Security

Currently, the application is exposed and vulnerable to many attacks.
Please set up WSS and sanitizes your input. Learn more about WebSocket Security here.

Conclusion

It's possible to use Hotwire through a new application, as we’ve shown here, but it's possible to update from Turbolinks.

This article should have helped you understand how the “magic framework” works. In it, you had the possibility to develop, in practice, a small simple application that shows requests via an aggregated turbo stream with manipulation and navigation via WebSocket.

Source: Honeybadger