DEV Community

Paul-Etienne Coisne
Paul-Etienne Coisne

Posted on • Edited on

10 6

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!

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

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

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay