An Interest In:
Web News this Week
- April 19, 2024
- April 18, 2024
- April 17, 2024
- April 16, 2024
- April 15, 2024
- April 14, 2024
- April 13, 2024
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
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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To