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!
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!
Top comments (1)
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 calledPostsWrapper.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.