Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
July 29, 2021 04:12 pm GMT

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 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.jsximport 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.rbget '/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.rbdef 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.rbclass 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  endend

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.jsimport 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.rbrequire "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  endend

The Redis instance has (or not) a "config" file:

#config/cabledevelopment:  adapter: redis  url: <%= ENV.fetch("REDIS_CABLE", "redis://:secretpwd@localhost:6379/3" ) %>  channel_prefix: cable_devproduction:  adapter: redis  url: <%= ENV.fetch("REDIS_CABLE", "redis://redis:6379/3" ) %>  channel_prefix: cable_prod
#/cable/config.rurequire_relative "../config/environment"Rails.application.eager_load!run ActionCable.server

Then run for example with overmind the Procfile, with overmind start

#Procfileassets:  ./bin/webpack-dev-serverweb:     bundle exec rails serverredis-server:   redis-server redis/redis.confworker:  bundle exec sidekiq -C config/sidekiq.ymlcable: bundle exec puma -p 28080 cable/config.ru

Happy coding!


Original Link: https://dev.to/ndrean/realtime-rails-with-websockets-1jk3

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To