Will Little Subscribe

Rails, ActionCable, Redux, and React: Walking through an example chat application


I spent some time over the holiday break wrapping my head around Rails 5 with Redux. This will be an interesting stack to consider.

Source code for the chat app is here. Live demo is here.

For those unfamiliar, Rails 5 introduces ActionCable (integrated websockets), and DHH published a quick demo of how it works. If you happen to be interested in diving into my example below on your own local machine (which essentially transforms DHH's example into Redux/React), I'd recommend spending a few minutes watching his webcast first, as I borrow some of the exact code.

Redux is an implementation of Facebook's Flux architecture (i.e. a way to help web developers manage the state and flow of data around an application). I'm among the ranks of JavaScript developers that have built front-end applications that grow insanely slow as the project grows, so I very much appreciate what Redux + React can do for us. It's hard to manage the effect of tons of asynchronous actions without introducing annoying UI bugs.

HOW REDUX AND REACT WORK TOGETHER

I won't go into the nuances of Flux and React here, so if you're unfamiliar with them make sure to do some background reading.

Assuming you understand the basic idea, here's a great diagram of how Redux simplifies the architecture:

In other words, React is fantastic for keeping UIs snappy and organized, but web developers are left to keep track of the data and state of the application on their own.

This is where Redux comes in: an action from the UI is combined with a simple JavaScript object (the state) to produce — via a reducer — a new state. That new state is then processed by react-redux and sent to React. That's it.

Now, under the hood, in the example Rails application I pieced together (modeled mostly from Kenta Suzuki's react-rails-redux-sample), the react-rails gem helps setup a folder structure in the asset pipeline that can be tweaked a bit for Redux like so:

Actions in Redux are functions that pass along the action type and whatever else you want to send to your reducer. In this example, the chat.js file in the actions folder looks like this:

export const ADD_MESSAGE = 'ADD_MESSAGE';

export const SET_MESSAGES = 'SET_MESSAGES';

export function setMessages(messages) {

 return {

   type: SET_MESSAGES,

   messages: messages

 };

}

export function addMessage(message) {

 return {

   type: ADD_MESSAGE,

   message: message

 };

}

view rawchat.js hosted with ❤ by GitHub

That is, we have an action that sets the messages up in the store (lines 4–9), and another that adds a message to the store (lines 11–16). That's it.

The reducer then looks like this:

import { SET_MESSAGES, ADD_MESSAGE } from '../actions/chat';

export default function chat(state = {}, action) {

 const { type, messages } = action;

 switch (type) {

 case ADD_MESSAGE:

   return [

     ...state,

     action.message

   ]

 case SET_MESSAGES:

   return messages

 default:

   return state;

 }

}

view rawchat.js hosted with ❤ by GitHub

Which contains a simple switch statement to send the action and the state into the right place in order to return the appropriate new state (a new object that your app listens to, which is important…you don't want to modify your old state).

At this point, if you really want to dive in, I'd recommend watching (for free) all 30 of Dan Abramov's intro videos. It takes a couple hours to plow through 'em, but your time is well spent; he walks through not only how Redux works, but why — from the ground up — it does what it does.

CONTAINERS AND COMPONENTS

Per Dan's videos, I went ahead and split up my React components into a “dumb” and “smart” component, termed component and container, respectively. The reason to do this, which Dan gets into, is to make it easier to map your state to properties (and dispatch binding) in the container, and let your “dumb” components just do the React (view-layer) stuff.

Thus, our chat app container file looks like this:

import { bindActionCreators } from 'redux';

import { connect } from 'react-redux';

import Chat from '../components/Chat';

import * as ChatActions from '../actions/chat';

function mapStateToProps(state) {

 return {

   messages: state.chat

 }

}

function mapDispatchToProps(dispatch) {

 return bindActionCreators(ChatActions, dispatch);

}

export default connect(mapStateToProps, mapDispatchToProps)(Chat);

view rawChatApp.js hosted with ❤ by GitHub

….which will make a ton more sense when you read the react-redux docs, but in short, it passes the part of the state that your “dumb” component should be concerned about, and ensures the proper dispatch binding so you can send actions to your reducers.

Thus, the “dumb” component of our chat app (now we're in React territory) is free to house the specific meat of our simple chat application:

import React, { Component, PropTypes } from 'react';

class Chat extends Component {

 render() {

   const { messages, addMessage } = this.props;

   const handleSubmit = (e) => {

     e.preventDefault();

   };

   const handleKeyUp = (e) => {

     if(e.keyCode == 13){

       App.room.speak(e.target.value);

       e.target.value = "";

     };

   };

   return (

     <div>

       <ul>

         {messages.map((msg) => {

             return <li key={`chat.msg.${msg.id}`}>{msg.content}</li>;

           })

         }

       </ul>

       <form onSubmit={handleSubmit}>

         <input type="text" onKeyUp={handleKeyUp}/>

       </form>

     </div>

   );

 }

}

Chat.propTypes = {

 messages: PropTypes.any

};

export default Chat;

view rawChat.js hosted with ❤ by GitHub

There are a few things going on in here, so I'll walk through ‘em:

  • I do a quick preventDefault() (line 10) so that our form doesn't submit when we press enter. :)
  • Lines 13–18 listens to every keystroke and submits our message to ActionCable when we press enter. The subscription to the web socket (i.e. the code that DHH uses) was setup within the componentWillMount() function here in the Root container.
  • Lines 21–31 are the rendered component itself, with some React-ness to display our list of messages and receive a new message typed in.

JBUILDER

The only other major tweak I did to translate DHH's tutorial into Redux/React is to use a couple jbuilder files:

….where index.json.jbuilder looks like this:

json.messages(Message.limit(10).order("created_at DESC").load.reverse) do |message|

 json.partial! 'messages/message', message: message

end

view rawindex.json.jbuilder hosted with ❤ by GitHub

(so we load the last 10 messages)…and the message partial is simply this:

json.extract! message, :id, :content

This way our index.html.erb file can simply be:

<%= react_component(Root',
    render(template: “messages/index.json.jbuilder”) %>

…so when our Rails app loads, it sends that initial state (the message objects in JSON format) to our Redux/React application.

Finally, in the MessageBroadCastJob file that DHH talks about, I just tweak it a bit to return JSON:

class MessageBroadcastJob < ApplicationJob

 queue_as :default

 def perform(message)

   ActionCable.server.broadcast 'room_channel', message: ActiveSupport::JSON.decode(render_message(message))

 end

 private

   def render_message(message)

     ApplicationController.renderer.render(partial: "messages/message.json.jbuilder", locals: {message: message})

   end

end

view rawmessage_broadcast_job.rb hosted with ❤ by GitHub

If you're interested in seeing a live version of this app, check out https://rails5reduxchat.herokuapp.com/ (this is hosted on Heroku's free tier, so if no one has hit the app for awhile it may take 20 seconds or so to fire up).

It took a bit of work to get the production environment setup for Redis/ActionCable, so check out specifically how I tweaked the cable.coffee file and updated the production.rb config file to include the action_cable configs for production. All-in-all it wasn't as bad as the tutorials out there say it can be, largely because most of the heavy lifting has already been incorporated recently into Rails 5.

Finally, if you're bold enough to try this on on your local machine, follow the instructions within the source code README file here.

Author's note: I'd love to hear from anyone with suggestions on how to improve the code/conventions used. While I'm now following the react-redux-universal-hot-example project and appreciate what the authors are up to there, it will take more time to loop in the [awesome] patterns I see (e.g. redux modules). Finally, subscribe to my newsletter and I'll let you know when I get new content up. Let's keep the conversation going. Thanks!