Search code examples
reactjsformsrails-apiactioncable

Rails API with ActionCable (WS) and React Front End


I have setup WebSockets through ActionCable for my Rails API/React Front End application and I am struggling with some real odd functionality. Currently, my <ActionCableConsumer /> is firing randomly. Let's say I have four browsers open and I submit a form that should render new data to all four browsers, two may stream the data from the channel, while the other two render nothing. Or one of the browsers may render the data more than once, while the other three render nothing. It truly is a sporadic deal.

Here is where I take the data from the subscription and update the state:

    handleReceived = (message) => {
      console.log('check me')
      this.setState({
        apiData: [...this.state.apiData, message.pickup_delivery]
     });
    }


  render(){
    return(
      <div>
      <ActionCableConsumer channel={{channel: 'PickupDeliveriesChannel'}} onReceived={this.handleReceived}>
        <div>
          <CustomersForm showCustForm={this.state.showCustForm} handleClose={this.hideForm} addNewCustomer={this.addNewCustomer} />
          <Calendar getCustForm={this.getCustForm} getForm={this.getForm} deleteLoad={this.deleteLoad} {...this.state} seededColorGenerator={this.seededColorGenerator} />
          <PickupDeliveriesForm getFormColor={this.getFormColor} deleteCustomer={this.deleteCustomer} handleClose={this.hideForm} showForm={this.state.showForm} updateLoad={this.updateLoad} {...this.state} onNewLoad={this.addNewLoad} seededColorGenerator={this.seededColorGenerator} />
        </div>
      </ActionCableConsumer>
      </div>
    )
  }
}

Here is what my controller looks like:

module Api::V1
  class PickupDeliveriesController < ApplicationController
 before_action :set_pickup_delivery, only: [:show, :update, :destroy]
  # GET /pickup_deliveries
    def index
      @pickup_deliveries = PickupDelivery.all

      render json: @pickup_deliveries
    end

  # GET /pickup_deliveries/1
    def show
      render json: @pickup_delivery
    end

  # POST /pickup_deliveriesPickupDeliveries
    def create
    pickup_deliveries = PickupDelivery.new(pickup_delivery_params)
    if pickup_deliveries.save
      serialized_data = ActiveModelSerializers::Adapter::Json.new(
        PickupDeliveriesSerializer.new(pickup_deliveries)
      ).serializable_hash
      ActionCable.server.broadcast 'pickup_deliveries_channel', serialized_data
      head :ok
    end
  end

  # PATCH/PUT /pickup_deliveries/1
    def update
      if @pickup_delivery.update(pickup_delivery_params)
        render json: @pickup_delivery
      else
        render json: @pickup_delivery.errors, status: :unprocessable_entity
      end
    end

  # DELETE /pickup_deliveries/1
    def destroy
      @pickup_delivery.destroy
    end

    private
    # Use callbacks to share common setup or constraints between actions.
      def set_pickup_delivery
       @pickup_delivery = PickupDelivery.find(params[:id])
      end

    # Only allow a trusted parameter "white list" through.
      def pickup_delivery_params
        params.require(:pickup_delivery).permit(:id, :pickup_date, :pickup_location, :rate, :delivery_date, :delivery_location, :local_delivery, :local_pickup, :loaded_miles, :deadhead_miles, :delivery_id, :pickup_zip, :delivery_zip, :hazmat, :sameday, :delivery_time, :pickup_time, :aurora_number, :out_of_route, :round_trip, :customer_id, :color)
      end
  end
end

Here is my actual channel:

class PickupDeliveriesChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'pickup_deliveries_channel'
  end
end

I am not specifying any users because I don't need to yet. Basically, when a client submits a new load on the load board, I need all the other load boards to display the new instance. Currently, it is not working like that all.

With five browsers open and after one form submission on a random browser, three out of the five browsers rendered the correct data (one load), one browser rendered nothing and one browser rendered the data TWICE.

It is pure chaos lol.

PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
PickupDeliveriesChannel transmitting {"pickup_delivery"=>{"id"=>17, "pickup_date"=>"2019-11-16", "pickup_location"=>"", "rate"=>nil, "delivery_date"=>"2019-11-16", "delivery_location"=>"", "local_delivery"=>false, "local_pickup"=>false, "loaded_miles"=>nil, "deadhead_miles"=>nil, "delivery_id"=>nil, "pickup_zip"=>"", "delivery_zip"=... (via streamed from pickup_deliveries_channel)
Unsubscribing from channel: {"channel":"PickupDeliveriesChannel"}
PickupDeliveriesChannel is transmitting the subscription confirmation
PickupDeliveriesChannel is streaming from pickup_deliveries_channel
PickupDeliveriesChannel stopped streaming from pickup_deliveries_channel
Unsubscribing from channel: {"channel":"PickupDeliveriesChannel"}
PickupDeliveriesChannel is transmitting the subscription confirmation
PickupDeliveriesChannel is streaming from pickup_deliveries_channel
PickupDeliveriesChannel stopped streaming from pickup_deliveries_channel
Unsubscribing from channel: {"channel":"PickupDeliveriesChannel"}
Unsubscribing from channel: {"channel":"PickupDeliveriesChannel"}
PickupDeliveriesChannel stopped streaming from pickup_deliveries_channel
PickupDeliveriesChannel stopped streaming from pickup_deliveries_channel
Unsubscribing from channel: {"channel":"PickupDeliveriesChannel"}
Could not execute command from ({"command"=>"unsubscribe", "identifier"=>"{\"channel\":\"PickupDeliveriesChannel\"}"}) [RuntimeError - Unable to find subscription with identifier: {"channel":"PickupDeliveriesChannel"}]: C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/subscriptions.rb:78:in `find' | C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/subscriptions.rb:46:in `remove' | C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/subscriptions.rb:18:in `execute_command' | C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/actioncable-5.2.3/lib/action_cable/connection/base.rb:87:in `dispatch_websocket_message' | C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/actioncable-5.2.3/lib/action_cable/server/worker.rb:60:in `block in invoke'
PickupDeliveriesChannel is transmitting the subscription confirmation
PickupDeliveriesChannel is streaming from pickup_deliveries_channel
Unsubscribing from channel: {"channel":"PickupDeliveriesChannel"}
PickupDeliveriesChannel stopped streaming from pickup_deliveries_channel
PickupDeliveriesChannel is transmitting the subscription confirmation
PickupDeliveriesChannel is streaming from pickup_deliveries_channel

On that specific submission, here is what my rails console looks like.

It is pure chaos and I have no idea how to make it flow correctly.

EDIT: The more form submissions I go through, the more subscriptions are being opened. It's almost like every time I open the form to submit something, another subscription is streaming? With two browsers open, I can be transmitting any number of objects...

Here are my routes:

Rails.application.routes.draw do
    namespace :api do
      namespace :v1 do
        resources :pickup_deliveries
        resources :customers
        end
    end
    mount ActionCable.server => '/cable'
end

Here is my index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ActionCableProvider } from 'react-actioncable-provider';
import * as serviceWorker from './serviceWorker';
import { API_WS_ROOT } from './constants';

ReactDOM.render(
  <ActionCableProvider url={API_WS_ROOT}>
    <App />
  </ActionCableProvider>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
serviceWorker.unregister();

Here are the constants that I am using

export const API_ROOT = 'http://localhost:3001/api/v1';
export const API_WS_ROOT = 'ws://localhost:3001/cable';
export const HEADERS = {
  'Content-Type': 'application/json',
  Accept: 'application/json',
};

Solution

  • So, I figured it out. I sent the current state that contained all my fetched instances VIA params from the front end to the channel and compared it with the current database. To be honest, I am not exactly sure as to why it worked. Basically, I said if params[:all_loads] (current fetched response in the front end) is equal to the amount of instances in the back end object, then continue to stream to channel. The solution is below:

    class PickupDeliveriesChannel < ApplicationCable::Channel
      def subscribed
        if params[:all_loads].length == PickupDelivery.all.length
            stream_from 'pickup_deliveries_channel'
        end
      end
    
      def unsubscribed
    
      end
    end