DEV Community

loading...

How to Build a Twitter Clone with Rails, ActionCable and React

rob__race profile image Rob Race Originally published at robrace.dev ・5 min read

Let me start by saying I think the things the teams working on CableReady and StimulusReflex are pretty awesome. They are aiming to make working with Reactive web applications as easy as the first Rails Blog tutorials during Rails' infancy.

With all of that being said, I am someone who prefers a bit more explicitness and work with tools I already know (and well, a lot more developers in the ecosystem).

I am not a video guy, don't have a video set up, and really prefer not to hear the sound of my own voice. So this will be done through words!

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

rails new blabber --no-spring --webpack=react

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

We can jump right into making a model to hold the data to clone what would be a tweet in this Twitter clone. All basic attributes:

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

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

class Post < ApplicationRecord
  validates :body, length: { minimum: 1, maximum: 280 }
end

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):

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!

rails db:migrate

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

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

  def create
    Post.create(post_params)
    ActionCable.server.broadcast(
      'PostsChannel',
      Post.all.order(created_at: :desc)
    )
    redirect_to posts_path
  end

  def like
    Post.find_by(id: params[:post_id]).increment!(:likes_count)
    ActionCable.server.broadcast(
      'PostsChannel',
      Post.all.order(created_at: :desc)
    )
    redirect_to posts_path
  end

  def repost
    Post.find_by(id: params[:post_id]).increment!(:repost_count)
    ActionCable.server.broadcast(
      'PostsChannel',
      Post.all.order(created_at: :desc)
    )
    redirect_to posts_path
  end

  private

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

Simple controller. The index action returns a list of posts, to @post. create uses StrongParameters, creates a new Post, broadcasts a message over Websockets (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 obviously work around these issues in a production application.

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 the action_cable_meta_tag and Bootstrap CDN CSS. This way, we can wire up some UI interfaces pretty quick!

<!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">
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= action_cable_meta_tag %>
  </head>

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

First up is the app/views/posts/index.html.erb:

<div class="container">
  <h1>Blabber</h1>
  <h4>A Rails, Actioncable and React demo</h4>

  <%= render partial: 'form' %>

  <%= react_component("PostsWrapper", { posts: @posts }) %>

</div>

react_component( is a view helper that is included in react-rails, a gem we will install in a minute. Even if you don't use every feature in the gem, it offers a great way to include a component into an existing view file and the props for its first load.

Up next is a straightforward Rails form:

<%= form_with model: @post, local: true, html: {class: 'my-4' } do |f| %>
<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 %>

Alright, that is all we need with ERB files, no we can move onto the ActionCable pieces.

First, we'll edit the Connection file to identify the Cable connection with the browser session ID:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :session_id

    def connect
      self.session_id = request.session.id
    end
  end
end

Next, we'll create a new Posts channel:

rails g channel PostsChannel

...and specify the channel we will be using in the stream by a string, PostsChannel:

class PostsChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'PostsChannel'
  end
end

That does it for the Actioncable backend!

Next up us the React side of the application. As we installed React with the Rails application command, we can jump to making sure that react-rails gem in installed:

gem 'react-rails'
gem 'webpacker', '~> 4.0'

With the Gemfile update, we can install the gem, use its installer, and add a package to helper connect to ActionCable in functional React components.

bundle install
rails generate react:install
yarn add use-action-cable

Almost there! We have two React components to add. First up, is a wrapper component that will allow you to wrap your true child component in the ActionCableProvider, which provides access to ActionCable through a React Context:

import React from "react";
import Posts from "./Posts";
import { ActionCableProvider } from "use-action-cable";

export default function PostsWrapper(props) {
  return (
    <ActionCableProvider url="/cable">
      <Posts {...props} />
    </ActionCableProvider>
  );
}

Inside the provider, there it passes the props to a Posts component. The Post component:

import React, { useState } from "react";
import PropTypes from "prop-types";
import { useActionCable } from "use-action-cable";

const Posts = props => {
  const [posts, setPosts] = useState(props.posts);

  const channelHandlers = {
    received: data => {
      console.log(`[ActionCable] [Posts]`, data);
      setPosts(data);
    }
  };

  useActionCable({ channel: "PostsChannel" }, channelHandlers);

  return (
    <React.Fragment>
      {posts.map(post => {
        return (
          <div key={post.id} className="card mb-2">
            <div className="card-body">
              <h5 className="card-title text-muted">
                <small className="float-right">
                  Posted at {post.created_at}
                </small>
                {post.username}
              </h5>
              <div className="card-text lead mb-2">{post.body}</div>
              <a className="card0link" href={`/posts/${post.id}/repost`}>
                Repost ({post.repost_count})
              </a>
              <a className="card0link" href={`/posts/${post.id}/like`}>
                Likes ({post.likes_count})
              </a>
            </div>
          </div>
        );
      })}
    </React.Fragment>
  );
};

Posts.propTypes = {
  posts: PropTypes.array,
  header_display: PropTypes.string
};

export default Posts;

This might be the most complicated file in the whole tutorial! First, we set up some internal state for Posts. This allows us to set the incoming posts prop as the state, to update when ActionCable passes new posts from a broadcast. channelHandlers sets up the handler for this ActionCable subscription to handler new data. Finally, for ActionCable setup, useActionCable ties the handler and channel name into a new front-end subscription.

The return statement returns the JSX template for each post. It is mostly Bootstrap markup but does include two links to reach the earlier created controller actions. As GET requests, they will follow the redirect and reload the index template.

There you go, at this point, it should look like this!

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!

Discussion

pic
Editor guide
Collapse
tonydehnke profile image
Tony Dehnke

Not really clear what files you are using from this point on: ActionCableProvider, which provides access to ActionCable through a React Context:

Update:
So played around trying out some file locations and names. Using ActionCableProvider.js and Posts.js saved in the app/javascript/components folder I can get thing to compile and load. However on submitting a post, I don't get it showing. In my console I get a couple errors:

Error: Cannot find module './PostWrapper'
ReferenceError: PostsWrapper is not defined

Update 2 - the file I had guessed at naming ActionCableProvider.js should be called PostsWrapper.js

Final - here is my repo with things working incase it can help someone else:

github.com/tonydehnke/Blabber-RobR...

NOTES: I made a mistake on my DB migration - what is likes_count in Rob's documentation is likes_counter in my code.