How to Build a Twitter Clone with Rails and Hotwire
Tags: rails, ruby, hotwire, webdev
Published: 2023-07-28 07:51:14

Hotwire is fresh out of the Basecamp Github and has set the Rails community ablaze. This quick write up is an update from [How to Build a Twitter Clone with rails, ActionCable, and React](https://robrace.dev/build-a-twitter-clone-with-rails-actioncable-and-react/) to show a comparison between the two approaches.

Now, this doesn't mean to throw away any React code you may be using or StimulusReflex if you have already gone down that path. Those approaches are both more mature and widely discoverable.

I may follow this up with a video to do a more side-by-side look at these two approaches.

Without further ado, let's get started with a new app:

```console
rails new blabber --no-spring
```

Rails will do its thing, install the application, install the gems, process the Webpacker install, and install the NPM packages.

Next, we will want to add the `hotwire-rails` gem to get the libraries needed:

```
gem "hotwire-rails", "~> 0.1.1"
```

Bundle and run the installer command for Hotwire:

```console
bundle install
rails hotwire:install
```

Now, let's make a model to hold the data to clone a tweet in this Twitter clone called Blabber. All basic attributes:

```console
rails g model Post username body:text likes_count:integer repost_count:integer
```

To keep this closely resembling the CableReady/StimulusReflex/React article, we'll add the same validation in the `Post` model:

```ruby
class Post < ApplicationRecord
  after_create_commit { broadcast_prepend_later_to 'posts' }
  after_update_commit { broadcast_replace_later_to 'posts' }
  after_destroy_commit { broadcast_remove_to 'posts' }

  validates :body, length: { minimum: 1, maximum: 280 }
end
```

One difference you see is the callbacks. When using Hotwire, the prevailing convention is to use `ActiveRecord` callbacks. While working on the model collection, you can set up callbacks for create, update, and destroy broadcasts.

We'll make a few small adjustments to the generated migration file to add some database-level defaults (and allows us to keep the code around `Post` creation simple):

```ruby
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :username, default: 'Blabby'
      t.text :body
      t.integer :likes_count, default: 0
      t.integer :repost_count, default: 0

      t.timestamps
    end
  end
end
```

Ok! Now, we're ready to run that migration!

```console
rails db:migrate
```

With the Model and Database layer out of the way, we can move on to the controller and corresponding view templates!

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

def create
  @post = Post.new(post_params)
  respond_to do |format|
    if @post.save
      format.html { redirect_to posts_path }
    else
      format.html { render :index }
      format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) } ## New for this article
    end
  end
end

def like
  Post.find_by(id: params[:post_id]).increment(:likes_count).save ## New for this article
  redirect_to posts_path
end

def repost
  Post.find_by(id: params[:post_id]).increment(:repost_count).save ## New for this article
  redirect_to posts_path
end

private

def post_params
  params.require(:post).permit(:body)
end
```

Simple controller. The `index` action returns a list of posts to `@post`. `create` uses `StrongParameters`, creates a new Post, and addresses some  `Turbo::Stream` usage (more on that soon), and redirects back to the `index` template. `like` and `repost` are similar except they increment the respective count columns.

Let's wire up a few routes to match up to those controller actions. Yes, these aren't perfect RESTful routes, but 1) They work. 2) This is a 10-minute tutorial. 3) Are GET requests making sure we do not have to worry about AJAX/fetch/CSRF in the front-end. You would work around these issues in a production application.

```ruby
Rails.application.routes.draw do
  resources :posts, only: %i[index create] do
    get 'like'
    get 'repost'
  end

  root to: 'posts#index'
end
```

With a Model, Controller, and routes, we can put together some view templates. We'll start by adding `Bootstrap` CDN CSS. This way, we can wire up some UI interfaces pretty quickly!

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Blabber</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <%= yield :head %>
    <%= turbo_include_tags %>
    <%= stimulus_include_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

The `yield :head`, `turbo_include_tags`, and `stimulus_include_tags` were all added earlier by the `hotwire:install` command.

The first view template up is the `app/views/posts/index.html.erb`:

```html
<div class="container">
  <h1>Blabber</h1>
  <h4>A Rails, Hotwire demo</h4>

  <%= turbo_frame_tag 'post_form' do %>
    <%= render partial: 'form' %>
  <% end %>

  <%= turbo_stream_from :posts %>
  <%= turbo_frame_tag 'posts' do %>
      <%= render @posts %>
  <% end %>
</div>
```

The view is a relatively simple usage of Rails, with a `render @posts` method, rendering the collection of `@posts`, using a `posts/_post` partial. The `turbo_frame_tag`'s will allow the `Turbo` libraries to instantiate the "frames" that will manage the HTML partials sent over the wire from the broadcasted changes. A lot is going on with that process, but Hotwire and Rails are taking care of it all and merely managing the markup. Specifically, `turbo_stream_from :posts` hooks up the view layer to listen to the Post model's broadcasts.

The partial is some pretty standard singular object markup with some Bootstrap classes thrown in to make it look half decent:

`app/views/posts/_posts.html.erb`

```html
<%= turbo_stream_from post %>
<div class="card mb-2" id="<%= dom_id(post) %>">
  <div class="card-body">
    <h5 class="card-title text-muted">
      <small class="float-right">
        Posted at <%= post.created_at %>
      </small>
      <%= post.username %>
    </h5>
    <div class="card-text lead mb-2"><%= post.body %></div>
    <a class="card-link" href="<%= post_repost_path(post) %>">
      Repost (<%= post.repost_count %>)
    </a>
    <a class="card-link" href="<%= post_like_path(post) %>">
      Likes (<%= post.likes_count %>)
    </a>
  </div>
</div>
```

Another `turbo_stream` command to listen for broadcasted updates and deletions, and the `dom_id` helper to create conventional Rails View IDs `model_resource-id`. In this case, it would look like `posts_1`, etc.

Up next is a straightforward Rails form:

```html
<%= form_with model: @post, id: dom_id(@post), html: {class: 'my-4' } do |f| %>
<% if @post.errors.any? %>
<div id="error_explanation">
  <h2>
    <%= pluralize(@post.errors.count, "error") %> prohibited this post from
    being saved:
  </h2>

  <ul>
    <% @post.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
    <% end %>
  </ul>
</div>
<% end %>
<div class="form-group">
  <%= f.text_area :body, placeholder: 'Enter your blab', class: 'form-control',
  rows: 3 %>
</div>

<div class="actions">
  <%= f.submit class: "btn btn-primary" %>
</div>
<% end %>
```

If you remember the `index.html.erb`, another frame wrapped the form's render call. Thus we included the `dom_id` to allow the controller's `create` action to enable Turbo to manage the streams' markup.

Now we'll head back to the controller to talk about the create action in depth:

```ruby
def create
  @post = Post.new(post_params)
  respond_to do |format|
    if @post.save
      format.html { redirect_to posts_path }
    else
      format.html { render :index }
      format.turbo_stream { render turbo_stream: turbo_stream.replace(@post, partial: 'posts/form', locals: { post: @post }) }
    end
  end
end
```

On a failed save, it will instruct the form's partial to update the form frame with the form template, but this time with the errors markup triggered.

At this point, it should look like this!

![Blabber Gif](https://robrace.dev/build-a-twitter-clone-with-rails-actioncable-and-react/blabber.gif)

There you go! I bet with a fast enough system to work through the Rails install, gem install, and javascript package installs, you could make it through this tutorial in less than 10 minutes!