DEV Community

loading...
Doctolib Engineering

Rails + React + ActionCable without the fuss

coisnepe profile image Paul-Etienne Coisne Updated on ・3 min read

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!

Discussion (4)

pic
Editor guide
Collapse
mowat27 profile image
Adrian Mowat

Excellent! Straight to the point. Exactly what I needed

Collapse
coisnepe profile image
Paul-Etienne Coisne Author

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
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.