Realtime Rails with websockets

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 re…


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

Rails guide standalone

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


Print Share Comment Cite Upload Translate Updates
APA

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/

MLA
" » Realtime Rails with websockets." NDREAN | Sciencx - Thursday July 29, 2021, https://www.scien.cx/2021/07/29/realtime-rails-with-websockets/
HARVARD
NDREAN | Sciencx Thursday July 29, 2021 » Realtime Rails with websockets., viewed ,<https://www.scien.cx/2021/07/29/realtime-rails-with-websockets/>
VANCOUVER
NDREAN | Sciencx - » Realtime Rails with websockets. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/07/29/realtime-rails-with-websockets/
CHICAGO
" » Realtime Rails with websockets." NDREAN | Sciencx - Accessed . https://www.scien.cx/2021/07/29/realtime-rails-with-websockets/
IEEE
" » Realtime Rails with websockets." NDREAN | Sciencx [Online]. Available: https://www.scien.cx/2021/07/29/realtime-rails-with-websockets/. [Accessed: ]
rf:citation
» Realtime Rails with websockets | NDREAN | Sciencx | 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.

You must be logged in to translate posts. Please log in or register.