Real-time likes with Turbo and Rails
Tags: rails, ruby
Published: 2023-09-17 19:13:56

---
title: Real-time likes with Turbo and Rails
published: true
date: 2023-09-15 15:51:01 UTC
tags: rails, ruby 
canonical_url: https://web-crunch.com/posts/adding-likes-rails-hotwire-turbo
---

{% youtube usoTj3EGGII %}

In this guide, my goal is to help you showcase the popularity of blog posts by adding real-time likes as a polymorphic feature to your Rails app using [hotwired.dev](https://hotwired.dev/) and the [Turbo](https://turbo.hotwired.dev/) framework. Follow this step-by-step guide to set up models, associations, and implement real-time liking functionality.

### Prerequisites

I'll use my new project called [Rails UI](https://railsui.com) alongside this guide, as it solves many of the early (and later) design problems for a typical Rails application. You're free to _not_ use Rails UI, but you'll need to install and configure [Devise](https://github.com/heartcombo/devise) (or a similar user authentication library) to achieve similar results.

### Getting Started

Let's generate a fresh Rails app. If you're using Rails UI, I highly recommend not including any JavaScript or CSS frameworks for maximum compatibility.

```bash
rails new hotwire_likes
```

**Add Rails UI**

I'll add the public alpha version of Rails UI with bundler. We can pass a couple of flags to pull directly from GitHub. Following that, we can run the Rails UI installer.

```bash
bundle add railsui --github getrailsui/railsui --branch main
rails railsui:install
```

When the installer completes boot, your server

```bash
bin/dev
```

Head to `localhost:3000`

You should see the Rails UI landing page and a button to configure your app. I chose a default Tailwind CSS theme called [`Hound`](https://hound.pages.dev/) Rails UI. As I mentioned before, the installer pre-installs Devise, which gives us a `User` model ready to work with under the hood.

### Setting Up Models and Associations

With the bulk of the setup out of the way, we need to focus more on the architecture of the application at hand.

To get started, we'll create two models for the app: `Post`, and `Like`. The `User` model will of course represent your app's users, the `Post` model will handle individual blog posts, and the Like model will keep track of likes given by users in a polymorphic fashion.

Execute the following commands in your terminal to generate these models:

```bash
rails generate scaffold Post title:string content:text
rails generate model Like user:references likeable:references{polymorphic}
```

After running these commands, don't forget to run `rails db:migrate` to apply the changes to your database.

### Defining Model Associations

Next, let's define the associations between these models. In the User model, add the following line:

```ruby
# app/models/user.rb
has_many :likes, dependent: :destroy
```

In the Like model, add the following lines:

```ruby
# app/models/like.rb
belongs_to :user
belongs_to :likeable, polymorphic: true
```

And in the Post model, add the following lines:

```ruby
# app/model/post.rb
has_many :likes, as: :likeable, dependent: :destroy
```

With these model associations in place, we can implement the logic for adding likes to posts using Hotwire and Turbo.

### Update Routing

Since we installed Rails UI, we can update the root path to be relative to the app at hand `root “posts#index”`

You can visit `/railsui/start` locally or click the Rails UI launcher in the bottom left of the viewport to access all Rails UI in your local development environment at any time after changing the default root path.

```ruby
# config/routes.rb
Rails.application.routes.draw do
  resources :posts
  if Rails.env.development? || Rails.env.test?
    mount Railsui::Engine, at: "/railsui"
  end

  # Inherits from Railsui::PageController#index
  # To overide, add your own page#index view or change to a new root
  # Visit the start page for Rails UI any time at /railsui/start
  #root action: :index, controller: "railsui/page"

  devise_for :users
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  root "posts#index"
end
```

### Implementing Liking Functionality

In your `PostsController`, create new actions called like and unlike to handle the liking functionality:

```ruby
class PostsController < ApplicationController
  before_action :set_post, only: %i[show edit update destroy like unlike]
  before_action :authenticate_user!, except: %i[index show]
  ...

  def like
    current_user.likes.create(likeable: @post)
    render partial: 'posts/post', locals: { post: @post }
  end

  def unlike
    current_user.likes.find_by(likeable: @post).destroy
    render partial: 'posts/post', locals: { post: @post }
  end
  ...
end  
```

The code in the `like` and `unlike` methods depend on an authenticated user, so we've added a callback function to be explicit with that:

```ruby
before_action :authenticate_user!, except: %i[index show]
```

With this line of code, we're telling our app that all routes beside `index` and `show` will require an authenticated user.

Additionally, we need an instance of the `@post` to perform the liking or unliking functionality. I extended the default before action to `set_post` to add our two new methods:

```ruby
before_action :set_post, only: %i[show edit update destroy like unlike]
```

Finally, after performing the logic, we render a basic `_post.html.erb` partial as a response. That file is in your `app/views/posts` folder, which was scaffolded when we started building the app.

Depending on your app, you might not necessarily want to “[stream](https://turbo.hotwired.dev/handbook/streams)” updates, which is fairly common in this scenario. Assuming you’ve added the proper turbo\_frame\_tag in your partials with the right id attributes, all you need to do in your controller is render a partial as a response. Hotwire is coined for being “HTML over the wire,” we're dumbing our controller code down to just that. Turbo frames take care of the rest, so you get real-time updates as expected. I love how simple this can become!

Now, if you **do** want to stream updates for a regular resource, you might need to render a traditional response with something like the following:

```ruby
  def like
    ....
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.replace(
        @post,
        partial: 'posts/post',
        locals: { post: @post }
        )
      end
      format.html { redirect_to @post }
  end
```

### Add the like and unlike routing

Update your `routes.rb` file to include a route for the `like` and `unlike` actions. We’ll leverage the `member` block to pass the appropriate `post_id` in the request.

```ruby
# config/routes.rb

resources :posts do
  member do
    post "like", to: "posts#like" # /posts/:id/like
    delete "unlike", to: "posts#unlike" # posts/:id/unlike
  end
end
```

This will create a route that maps to the like action on individual posts in the form of a `POST` request and an unlike action in the form of a `DELETE` request.

### Create some dummy content

At this point, we need a post or two to “like.” Create some dummy content to make this easier on ourselves.

```ruby
Post.create(title: "Boosting Web App Performance with Tailwind CSS", content: "In this blog post, we'll explore how to optimize the performance of your web applications using Tailwind CSS. We'll cover the basics of Tailwind's utility-first approach and demonstrate how it can help reduce your CSS file size and improve loading times.")

Post.create(title: "Building Dynamic Web Apps with Ruby on Rails and Stimulus.js", content: "Ruby on Rails and Stimulus.js make a powerful combination for building dynamic web applications. In this blog post, we'll explore how you can use Ruby on Rails as your backend framework and Stimulus.js as your frontend JavaScript framework to create interactive and responsive web apps. We'll cover topics like setting up your Rails project, integrating Stimulus.js, and building real-time features. Whether you're a seasoned developer or just starting, you'll find this guide helpful in taking your web development skills to the next level.")
```

### Enhancing the Views

Let’s improve the view for displaying a post by including a like button. In your `posts/_post.html.erb` file, add the following code:

```erb
<!-- app/views/posts/_post.html.erb-->

<%= turbo_frame_tag dom_id(post) do %>
  <article class="py-6 prose dark:prose-invert">
    <p class="mb-0 font-semibold">
      Title
    </p>
    <p class="my-0">
      <%= post.title %>
    </p>
    <p class="mb-0 font-semibold">
      Content
    </p>
    <p class="my-0">
      <%= post.content %>
    </p>
    <time class="text-slate-600 dark:text-slate-400 text-xs mt-2" datetime="<%= post.created_at.to_formatted_s(:long) %>">Created <%= time_ago_in_words(post.created_at) + " ago" %></time>

    <p><%= pluralize(post.likes.count, 'like') %></p>

    <% if user_signed_in? %>
      <% if current_user && post.likes.exists?(user_id: current_user.id) %>
        <%= button_to "Unlike", unlike_post_path(post), method: :delete, class: "text-rose-600" %>
      <% else %>
        <%= button_to "Like", like_post_path(post), method: :post, class: "like-button" %>
      <% end %>
    <% else %>
      <%= link_to "Like", new_user_session_path, class: "underline", data: { turbo_frame: "_top" } %>
    <% end %>
  </article>
<% end %>
```

This code will create a turbo frame for each post with a unique ID thanks to the post ID and the `dom_id` view helper. It also displays the post title, content, and the number of likes.

If the current user has already liked the post, it will display an "Unlike" button that sends a `DELETE` request to the unlike action in the `PostsController.` If the user hasn't liked the post, it will display a "Like" button that sends a POST request to the like action.

Any user will need to be signed in to like or unlike a post, and we’ll show a fake like link that links back to the sign-in form if the person happens to be visiting.

### Always room for improvement

While the example I shared is pretty simple, you could go on.e step further and broadcast updates using turbo stream and action cable. I haven’t found a good solution for doing this well with polymorphic relationships so I left that part out. I’ll circle back and update this post if I come across anything!

Don't forget to check out [railsui.com](https://railsui.com) to find the alpha version of the Ruby gem I [recently released](https://railsui.com/updates/new-themes-retriever-hound).