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
$ 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
# config/routes.rb
Rails.application.routes.draw do
mount ActionCable.server => '/cable'
resources :messages, only: %i(index create)
end
# 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:
# app/views/messages/index.html.erb
<%= javascript_packs_with_chunks_tag 'messages' %>
// 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.
// 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
// 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')))
})
# 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
:
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!
Top comments (5)
You can
import { csrfToken } from "@rails/ujs"
to get the "X-CSRF-TOKEN"On the same subject, the Hotwire/Turbo project is pretty neat.
Excellent! Straight to the point. Exactly what I needed
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.
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