DEV Community

Paul-Etienne Coisne for Doctolib Engineering

Posted on • Updated on

Rails + React + ActionCable without the fuss

The code for this article is on Github
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
Enter fullscreen mode Exit fullscreen mode
$ 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
Enter fullscreen mode Exit fullscreen mode
# config/routes.rb
Rails.application.routes.draw do
  mount ActionCable.server => '/cable'
  resources :messages, only: %i(index create)
end
Enter fullscreen mode Exit fullscreen mode
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index; end
end
Enter fullscreen mode Exit fullscreen mode
$ touch app/javascript/packs/messages.js
$ mkdir app/views/messages
$ touch app/views/messages/index.html.erb
Enter fullscreen mode Exit fullscreen mode

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

# app/views/messages/index.html.erb
<%= javascript_packs_with_chunks_tag 'messages' %>
Enter fullscreen mode Exit fullscreen mode
// 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')))
})
Enter fullscreen mode Exit fullscreen mode

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.

// 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
Enter fullscreen mode Exit fullscreen mode
// 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')))
})
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

Add a #create method in your MessagesController:

  def create
    Message.create(content: params[:message])
    ActionCable.server.broadcast('messages', { messages: Message.all })
  end
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (5)

Collapse
 
ndrean profile image
NDREAN

You can import { csrfToken } from "@rails/ujs" to get the "X-CSRF-TOKEN"
On the same subject, the Hotwire/Turbo project is pretty neat.

Collapse
 
mowat27 profile image
Adrian Mowat

Excellent! Straight to the point. Exactly what I needed

Collapse
 
coisnepe profile image
Paul-Etienne Coisne

Thanks for leaving me my first comment ever on dev.to 🚀 Didn't expect people to stumble upon this so quickly. And thanks for the kind words. I'm generally frustrated by long articles cluttered by gifs, memes and unnecessary diagrams, so I'm glad people appreciate this "straight-to-the-point" approach.

Collapse
 
mowat27 profile image
Adrian Mowat

Hi,

My pleasure. I always try and thank people for their hard work. I know how much work goes into even a short post like this one.

Cheers

Adrian

Collapse
 
dorianmariefr_22 profile image
Dorian Marié

Thanks, great!