This content originally appeared on DEV Community and was authored by NDREAN
Yet another tuto on Rails' framework ActionCable. I focus on going quickly to the relevant paths to achieve running a rails app with a realtime feature packaged as a standalone process.
Instead of a traditional chat app, this one simulates managing realtime inventories. It has a button that on-click increments a counter and broadcasts the decremented total; this simulates a customer fulling his basket and decreasing accordingly the visible stock to any other connected customer.
We will setup the backend and the frontend. The frontend requires the installation of the npm package actioncable, and the backend to enable the middleware action_cable/engine.
The frontend is managed by React, and the Websockets are managed by the integrated framework ActionCable.
The process is the following:
- on the frontend, implement a component with a button that triggers a POST request to a Rails backend endpoint,
- a Rails controller method responds to this route. It should:
- save the new value/customer to the database,
- calculate the new stock
- broadcast the total to a dedicated websocket channel
- in the frontend React component, we update the state of the stock :
- on each page refresh (a GET request to the database)
- when receiving data through the dedicated websocket channel.
The frontend component "Button.jsx" looks like:
//#Button.jsx
import React, { useState, useEffect } from "react";
import { csrfToken } from "@rails/ujs";
[...other imports..]
const Button = ()=> {
const [counter, setCounter] = useState({})
[...to be completed...]
const handleClick = async (e) > {
e.preventDefault()
await fetch('/incrmyprod',{
method: "POST",
headers: {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken(),
},
body: JSON.stringify(object),
})
return (
<>
<button onClick={handleClick}>
Click me!
</button>
{counters && (
<h1>PG counter: {counters.counter}</h1>
)}
</>
);
};
The backend
We run $> rails g channel counter
and have a "counter" model.
/app/channels
|_ /application_cable
|_ counter_channel.rb
In our routes, we link the frontend URI to an action:
#app/config/routes.rb
get '/incrmyprod', to: 'counters#set_counters'
mount ActionCable.server => '/cable'
In the controller's "counters" method "set_counters", we will broadcast the new data to the dedicated websocket channel:
#app/controllers/counters_controller.rb
def set_counters
[...]
data = {}
data['counter'] = params[:counter]
ActionCable.server.broadcast('counters_channel', data.as_json)
end
In the dedicated channel, we broadcast this data when received to all subscribed consummers:
#app/channels/counter_channel.rb
class CounterChannel < ApplicationCable::Channel
def subscribed
stream_from "counters_channel"
end
def receive(data)
# rebroadcasting the received message to any other connected client
ActionCable.server.broadcast('counters_channel',data)
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
stop_all_streams
end
end
In the layouts, add <%= action_cable_meta_tag %>
The frontend:
We installed npm i -g actioncable
. Since we ran rails g channel counter
, we have the files:
/javascript/channels
|_ consumer.js
|_ index.js
|_ counter_channels.js
#app/javascript/channels/counter_channel.js
import consumer from "./consumer";
const CounterChannel = consumer.subscriptions.create(
{ channel: "CounterChannel" },
{
connected() {
},
disconnected() {
},
received(data) {
// Called when there's incoming data on the websocket for this channel
},
}
);
export default CounterChannel;
In the Button component, we will mutate the state of the counter. On page refresh, we fetch from the database and mutate the state for rendering, and when we receive data on the websocket channel, we also mutate the state for rendering. To do this, we pass a function to the CounterChannel.received that mutates the state. If we don't have any data, then we mutate the state with a GET request. This is done wition a useEffect
hook. We can complet the Button component with:
import CounterChannel from "../../channels/counter_channel.js";
[...]
const Button = ()=> {
const [counters, setCounters] = useState({});
useEffect(() => {
async function initCounter() {
try {
let i = 0;
CounterChannel.received = ({ counter }) => {
if (counter) {
i = 1;
return setCounters({ counter });
}
};
if (i === 0) {
const { counter } = await fetch("/getCounters", { cache: "no-store" });
setCounters({ countPG: Number(countPG) });
}
} catch (err) {
console.warn(err);
throw new Error(err);
}
}
initCounter();
}, []);
[...the rest of the component above ...]
}
Standalone setup
For the frontend, run npm i -g actioncable
For the backend, enable the middleware and config:
#/config/application.rb
require "action_cable/engine"
[...]
module myapp
class Application < Rails::Application
[...]
config.action_cable.url = ENV.fetch('CABLE_FRONT_URL', 'ws://localhost:28080')
origins = ENV.fetch('CABLE_ALLOWED_REQUEST_ORIGINS', "http:\/\/localhost*").split(",")
origins.map! { |url| /#{url}/ }
config.action_cable.allowed_request_origins = origins
end
end
The Redis instance has (or not) a "config" file:
#config/cable
development:
adapter: redis
url: <%= ENV.fetch("REDIS_CABLE", "redis://:secretpwd@localhost:6379/3" ) %>
channel_prefix: cable_dev
production:
adapter: redis
url: <%= ENV.fetch("REDIS_CABLE", "redis://redis:6379/3" ) %>
channel_prefix: cable_prod
#/cable/config.ru
require_relative "../config/environment"
Rails.application.eager_load!
run ActionCable.server
Then run for example with overmind the Procfile, with overmind start
#Procfile
assets: ./bin/webpack-dev-server
web: bundle exec rails server
redis-server: redis-server redis/redis.conf
worker: bundle exec sidekiq -C config/sidekiq.yml
cable: bundle exec puma -p 28080 cable/config.ru
Happy coding!
This content originally appeared on DEV Community and was authored by NDREAN
NDREAN | Sciencx (2021-07-29T16:12:20+00:00) Realtime Rails with websockets. Retrieved from https://www.scien.cx/2021/07/29/realtime-rails-with-websockets/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.