Artur Chmaro

blog about web technologies

Push items to React component using ActionCable

I'm working on the WebChatter app and today I want to create a view that will display to consultant a list of all active chats with customers. Such lists are changing dynamically, because someone may connect or disconnect in a matter of seconds. I don't want to do polling on the client side, so I decided to use WebSockets. Rails5 are coming with a nice library for dealing with WebSockets called ActionCable. I want to use subscribe AS channel in my React component and add new chat to list each time a new customer show up.

Before we can start using ActionCable in Rails5 app some basic configuration is required. First, we need to configure a route for ActionCable in config/routes.rb:

mount ActionCable.server => '/cable'

Then we have to install redis gem:

gem 'redis'

Make sure that redis is up and running on our development machine:

Once it's done we can create our first channel:

# app/channels/chats_channel.rb
class ChatsChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'chats'
  end
end

Now let's install npm packet for ActionCable:

$ yarn add actioncable

The AS packet is ready to use, so we can focus on the React component:

import React, { PropTypes } from 'react'
import { Row, Col, Menu } from 'antd';
import { Chat } from './Chat';

export default class Chats extends React.Component {
  constructor(props, _railsContext) {
    super(props);
    this.state = {
      chats: [],
    };
  }

  renderChats() {
    const chats = this.state.chats;
    const chatItems = chats.map((chat, i) =>
      <Menu.Item key={i}>{chat.name} - {chat.email}</Menu.Item>
    );

    return chatItems;
  }
  render() {
    return (
      <div>
        <Row>
          <Col span={4}>
            <Menu mode="inline">
              {this.renderChats()}
            </Menu>
          </Col>
        </Row>
      </div>
    );
  }
}

Currently, my component is ready to render menu items each time an internal state of chats got changed. It is not exactly what we need, so let's add a subscription to ChatsChannel and mutate state once new chat arrives. First we need to import ActionCable from node_modules, then we have to subscribe to our channel and mutate on state each time the new message is received:

import React, { PropTypes } from 'react'
import { Row, Col, Menu } from 'antd';
import { Chat } from './Chat';
import ActionCable from 'actioncable';

export default class Chats extends React.Component {
  constructor(props, _railsContext) {
    super(props);
    this.state = {
      chats: [],
    };
    this.subscribeChannel()
  }

  addChat(chat) {
    this.state.chats.push(chat)
    this.setState({
      chats: this.state.chats
    });
  }

  subscribeChannel() {
    const cable = ActionCable.createConsumer('ws://localhost:3000/cable');
    cable.subscriptions.create('ChatsChannel', {
      received: (data) => {
        this.addChat(data.chat);
      }
    });
  }

  renderChats() {
    const chats = this.state.chats;
    const chatItems = chats.map((chat, i) =>
      <Menu.Item key={i}>{chat.name} - {chat.email}</Menu.Item>
    );

    return chatItems;
  }
  render() {
    return (
      <div>
        <Row>
          <Col span={4}>
            <Menu mode="inline">
              {this.renderChats()}
            </Menu>
          </Col>
        </Row>
      </div>
    );
  }
}

Now we can restart the server, run rails console in the background and see if we can push items to client from our backend. Just broadcast something to our channel:

ActionCable.server.broadcast "chats", { chat: {email: 'customer@bbc.com', name: 'John Doe' } }

via GIPHY

Looks like it is working. In the next iteration, I will try to move subscription logic from the React component and just pass an array of chats as a prop from Redux store.