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!