Digging into React and Redux with Rails 5’s ActionCable

One of the exiting new technologies in Rails 5 is ActionCable, essentially an implementation of web sockets that allows real time communication between client and server.

We’ve been re-implementing our helpdesk system by rebuilding the user interface as a React application.  A majority of the user interaction is viewing lists of tickets, which are being constantly updated by the helpdesk staff.  This seemed a perfect use of ActionCable to allow for live updating of people’s ticket lists.

The majority of tutorials embed the React app into the Rails app using gems such as react-rails.  This gives you access to the Rails generated Javascript App object for making ActionCable calls, and defining what happens when ActionCable messages are sent back inside of the Rails app using Coffeescript, but I wanted to build a more pure ReactJS app separate from the Rails application.

Our app is based on Redux, so the concept we wanted to investigate was sending updates to the server as an ActionCable message, and receive updated tickets back over the ActionCable channel, and have those feed back into the Redux store.

Rails and ActionCable

To start on the server side, we created a very simple ActionCable channel.  We skip the user authentication and separation of streams by user and simply broadcast all ticket changes to all subscribers.  We define two methods, send_all_tickets which can be used so set the initial store state (and for forcing refreshes) and new_ticket which is called when a ticket is added to the system.  In our full system we would add this ticket into the database, but for this proof of concept we just broadcast the new ticket out to all subscribers.

require 'securerandom'

class TicketsChannel < ApplicationCable::Channel
  def subscribed
    stop_all_streams
    stream_from "tickets"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def send_all_tickets
    Ticket.all.each do |ticket|
      message_data = {
        action: 'new_ticket',
        ticket: ticket
      }
      ActionCable.server.broadcast('tickets',message_data)
    end
  end


  def new_ticket(ticket)
    # normally would create a ticket in the database here
    
    ticket = {
      subject: ticket["ticket"],
      id: SecureRandom.uuid
    }
    message_data = {
      action: 'new_ticket',
      ticket: ticket,
    }
    ActionCable.server.broadcast('tickets',message_data)
  end

  private


end

On the React side, we assume we are working with a browser that supports WebSockets natively.

We start by setting up React and importing our Redux action for adding tickets to the store.

import { addTicketToStore } from '../actions'

We then implemented a simple WebSocket interface for receiving messages from ActionCable and for sending messages to the server and store it in the component state.


  componentDidMount() {
 
    let ws= new WebSocket("ws:localhost:3000/cable" )
    ws.onopen = function() {
        let identifier = JSON.stringify({channel:'TicketsChannel'})
        let msg = JSON.stringify({command:'subscribe', identifier:identifier})
        ws.send(msg);
     };

     ws.onmessage = (evt) => {
        var received_msg = evt.data;
        this.process_message(received_msg)
     }

     ws.onclose = function()
     {
        // websocket is closed.
        console.log("Connection is closed...");
     };

     this.setState({ws: ws})
  }

Any messages we receive from the Rails server are processed and the message information extracted out, and if the message is a new ticket, we add the ticket into the Redux store:

  process_message(received_msg) {

    let parsedMessage = JSON.parse(received_msg)
    if (parsedMessage["identifier"]!="_ping") {
      console.log("Message is received..." +  received_msg);
      if (parsedMessage["message"]) {
        let message=parsedMessage["message"]
        if (message['action']=='new_ticket') {
          this.props.dispatch(addTicketToStore(message["ticket"]))
        }
      }
    }

  }

and here we send a message to the server to retrieve all tickets

  get_all_tickets() {
    console.log('send request for all tickets')
    let identifier = JSON.stringify({channel:"TicketsChannel"})
    let data = JSON.stringify({action:'send_all_tickets'})
    let msg = JSON.stringify({command:'message', identifier:identifier,data:data})
    this.state.ws.send(msg);
  }

 

We render a simple interface for listing our tickets from the store :

 



      <div className="main-container">

        <div className="container">
          <a href='#' onClick={e => {
              e.preventDefault()
              console.log("refresh")
              this.get_all_tickets()
            }}>Refresh All tickets</a>
          <h3>Tickets:</h3>
          <ul>
          {this.props.tickets.map(function(ticket){
            return <li key={ticket.id}>{ticket.subject}</li>;
          })}
          </ul>
        </div>
        <div>

 

and finally render a simple interface to submit new tickets by sending a message to the server:

 


      <form onSubmit={e => {
        e.preventDefault()
        if (!input.value.trim()) {
          return
        }
        let identifier = JSON.stringify({channel:"TicketsChannel"})
        let data = JSON.stringify({action:'new_ticket', ticket:input.value})
        let msg = JSON.stringify({command:'message', identifier:identifier,data:data})

        this.state.ws.send(msg);
        input.value = ''
        }}>
        <input ref={node => {
          input = node
        }} />
        <button type="submit">
          Add Ticket
        </button>
      </form>

We end up with an interface like this:

Tickets List

As a new ticket subject is typed and “Add Ticket” is clicked, every browser open to this page updates with the new ticket added to the list.

This is just the beginning of how we see our Rails app and our ReactJS Redux app interacting, but we’re very excited but what’s possible. Our next steps are to make a generalised ActionCable component to feed actions to the store.

You can download the whole project from github here

Leave a Reply

Your email address will not be published. Required fields are marked *