Rails + React + ActionCable without the fuss
Tags: ruby, react, actioncable, websockets
Published: 2021-01-03 14:44:21

The code for this article is [on Github](https://github.com/coisnepe/reaction_cable)
If you've landed here I'm going to bet that you simply want to know how to add ActionCable to a Rails app running React with Webpacker, nothing more, nothing less. I also assume that you know Rails and React, so I will spare explanations.
This is meant to be the absolute bare minimum: I haven't added any gem or yarn package, no check on params, no authentication, etc. It's merely a help to jumpstart your project.

Let's cut to the chase!

```
$ rails new reaction_cable -T --webpack=react
$ rails g model Message content
$ rails db:setup
$ rails db:migrate
$ rails s
# In another terminal…
$ webpack-dev-server
```

```
$ touch app/controllers/messages_controller.rb
$ rails g channel messages
      create  app/channels/messages_channel.rb
   identical  app/javascript/channels/index.js
   identical  app/javascript/channels/consumer.js
      create  app/javascript/channels/messages_channel.js
```

```ruby
# config/routes.rb
Rails.application.routes.draw do
  mount ActionCable.server => '/cable'
  resources :messages, only: %i(index create)
end
```

```ruby
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index; end
end
```

```
$ touch app/javascript/packs/messages.js
$ mkdir app/views/messages
$ touch app/views/messages/index.html.erb
```

Now that we have the files set up, let's fill them in:

```ruby
# app/views/messages/index.html.erb
<%= javascript_packs_with_chunks_tag 'messages' %>
```

```javascript
// app/javascript/packs/messages.js
import 'channels'
import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import MessagesChannel from 'channels/messages_channel'

const MessagesBoard = () => <div>Empty</div>

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(<MessagesBoard />, document.body.appendChild(document.createElement('div')))
})
```

At this point, http://localhost:3000/messages should be browsable, albeit empty :-)

Export the channel subscription in order to use it in the Messages component.

```javascript
// app/javascript/channels/messages_channel.js
import consumer from './consumer'

const MessagesChannel = consumer.subscriptions.create('MessagesChannel', {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
  },
})

export default MessagesChannel
```

```javascript
// app/javascript/packs/messages.js

import 'channels'
import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import MessagesChannel from 'channels/messages_channel'

const MessagesBoard = () => {
  const [messages, setMessages] = useState([])
  const [message, setMessage] = useState('')

  useEffect(() => { 
    MessagesChannel.received = (data) => setMessages(data.messages)
  }, [])

  const handleSubmit = async (e) => {
    e.preventDefault()
    // Add the X-CSRF-TOKEN token so rails accepts the request
    await fetch('http://localhost:3000/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('[name=csrf-token]').content,
      },
      body: JSON.stringify({ message }),
    })
    setMessage('')
  }

  return (
    <div>
      <input type="text" value={message} onChange={({ target: { value } }) => setMessage(value)} />
      <button onClick={handleSubmit}>Send message</button>

      <ul>
        {messages.map((message) => (
          <li key={message.id}>{message.content}</li>
        ))}
      </ul>
    </div>
  )
}

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(<MessagesBoard />, document.body.appendChild(document.createElement('div')))
})
```

```ruby
# app/channels/messages_channel.rb

class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'messages'

    ActionCable.server.broadcast('messages', { messages: Message.all })
  end

  def unsubscribed; end
end
```

Add a `#create` method in your `MessagesController`:
```ruby
  def create
    Message.create(content: params[:message])
    ActionCable.server.broadcast('messages', { messages: Message.all })
  end
```

You should now have a working Rails+React+ActionCable app 🚀
Please let me know in the comments if you'd like to know more about React+Rails+ActionCable!