Making Your React Rails Application Update Real Time
Many applications that are commonly used need to update real time, and we can’t achieve this using the HTTP protocol alone.
Meet WebSockets, a computer communication protocol that allows real data time transfer between server and the client. Unlike, the HTTP protocol where a communication happens via a request and response, WebSockets maintains a constant connection for data transfer. This allows the server to send content to the client without it being first requested by the client, facilitating real-time data transfer.
In this blog post, I will be creating a real-time message board using React/Rails client/server and using Rails ActionCable and JS native WebSockets class. The application will not include Redux for simplicity. For anyone interested in integrating WebSockets with React Redux, I found this article very helpful.
The application (GitHub) lists the current messages and any new posts will be updated real-time:
What is Rails ActionCable?
In Rails, ActionCable is what allows a connection to the server via WebSockets.
Users connect to the ActionCable server via the route specified in config/routes.rb — we will get to that in a second. Once connected, the client or the consumer can create multiple “subscriptions” to one or more different “channels” using that connection. For example, a user connects to ActionCable server by visiting your website. Once connected they can subscribe to the “RoomsChannel”, “UsersChannel”, and/or “MessagesChannel” to receive live data from.
These channels then “broadcast” data, which gets sent to anyone that is subscribed.
This is called a publisher/subscriber communication paradigm. Anyone subscribed to a channel will receive the data when the publisher broadcasts data — there is no specification of individual recipients of data.
Rails API
I generated new application called “websockets” with resource message, which has attribute :content. Database has been setup with basic seed data. Make sure to enable CORS.
rails new websockets --api
bundle install
rails g resource message content:string
We need to tell Rails how client will connect to ActionCable server:
/cable will be routed to ActionCable server.
#config/routes.rb
Rails.application.routes.draw do
resources :messages
mount ActionCable.server => '/cable'
end
Next, we need to create a channel that the consumer will subscribe to: MessagesChannel.
rails g channel Messages
This will generate messages_channel.rb in app/channels folder, with the methods subscribed and unsubscribed. This is what my MessagesChannel looks like after adding the actions I will need for subscribing and creating a new message.
#app/channels/messages_channel.rbclass MessagesChannel < ApplicationCable::Channel
def subscribed
stream_from "messages"
messages = Message.all
ActionCable.server.broadcast("messages", {type: "current_messages", messages: messages})
end def unsubscribed
# Any cleanup needed when channel is unsubscribed
end def create_message(data)
message = Message.create(content: data["content"])
messages = Message.all
ActionCable.server.broadcast("messages", {type: "current_messages", messages: messages})
end
end
There are two methods to note: subscribed, and create_message.
- subscribed() will be invoked when consumer subscribes to the Messages Channel. subscribe() will begin streaming from the stream called “messages,” which means that now anything that gets broadcasted to “messages” will be received by this subscriber.
Also, it will make an initial broadcast, so that once the consumer is subscribed they receive an initial broadcast of all the messages.
ActionCable.server.broadcast takes 1st arg: the stream to broadcast to and 2nd arg: the content. In this example, the content object specifies the “type” and “messages”. - Next, the create_message action will be used to create a new message using form input from the client, and also *********rebroadcast the new updated array of messages to the “messages” stream. This is the core of real-time updates: rebroadcasting the new instance (or whatever changes) to all the current subscribers upon creation.*********
Create Client
Created React application called ‘client’. Starting server and client.
npx create-react-app client
rails s -p 3001
cd client && npm start
Connecting and Subscribing using WebSockets and React
The overall flow of connecting and subscribing client side:
- When App component mounts, it will start connecting to Rails ActionCable via the url, ws://localhost:3001/cable.
- The WebSocket connection will be made using native JS class WebSocket. By attaching handlers like onopen, onclose, and onmessage, we can instruct what to do when the connection is successful or when connection receives data from the server.
- Using a Promise, I will subscribe to MessagesChannel AFTER the WebSocket has successfully connected.
- Once subscribed to the channel, any data that gets broadcast will be handled by the onmessage handler. Here, the callback function for onmessage will set state.messages to the received data, which gets rendered by component.
// /client/src/App.js
class App extends React.Component {
state = {
newMessage: "",
messages: [],
socket: null
} submitHandler = event => {
event.preventDefault();
//=> send state.newMessage to channel via WebSockets- will get to later
} changeHandler = event => {
this.setState({
newMessage: event.target.value
})
} render() {
return (
<div className="App">
Messages Board:
<ul>
{this.renderMessages()}
</ul>
<form onSubmit={this.submitHandler}>
<input type="text" onChange={this.changeHandler} value {this.state.newMessage} name="newMessage"/>
<input type="submit" value="Post"/>
</form>
</div>
);
}
}export default App;
This is the component App that handles all the connections and rendering. I have renderMessages(), which will render state.messages as <li>. I have a state controlled form that will send the state.newMessage to the server via WebSockets. States have attributes messages, newMessage, and socket.
***********EDIT: I realized the WebSocket JS class is not very compatible with Rails ActionCable. It wouldn’t allow for basic things like unsubscribing from a ActionCable channel. Therefore, I had to switch over to actioncable to create the connection and create subscriptions. Check out the edited component here => App.js,
I also found Daniel’s blog post very helpful in utilizing actioncable in my components.***************
// /client/src/App.js
//... in App ComponentconnectToActionCable = host => {
return (
new Promise ((resolve, error) => {
//create and connect
let socket = new WebSocket(host); //handlers
socket.onopen = () => {resolve(socket)};
socket.onclose = () => {console.log('ws closed')};
socket.onerror = errors => {error(errors)};
socket.onmessage = event => { let payload = JSON.parse(event.data);
if (payload.message){
switch (payload.message.type) {
case 'current_messages':
this.setState({
messages: payload.message.messages
})
break;
default:
break;
}
}
};
})
)
}componentDidMount(){
this.connectToActionCable(`ws://localhost:3001/cable`)
.then(socket => {
this.setState({
socket: socket
}) const subscribe_info = {
command: 'subscribe',
identifier: JSON.stringify({channel: "MessagesChannel"})
}
socket.send(JSON.stringify(subscribe_info));
});
}
...# app/channel/messages_channel.rb
...
def subscribed
# stream_from "some_channel"
messages = Message.all
stream_from "messages"
ActionCable.server.broadcast("messages", {type: "current_messages", messages: messages})
end
...
On componentDidMount(), I will call connectToActionCable, which returns a Promise. The promise creates a new WebSocket connection with handlers onopen, onclose, onerror, onmessage. Take note of onopen and onmessage as we will need these later.
onopen will resolve the promise passing in the socket connection.
The objects broadcasted by channel (ActionCable.server.broadcast(“stream_name”, data_object)) will be in event.data.message.
onmessage is called whenever data is sent to the connection from the server. Here, it is parsing data as JSON and using the data.message.type and a case statement to decide what to do — this structure is helpful for later on when you will be receiving multiple different types of data from ActionCable server i.e. “delete” or “create”. The “current_messages” case will set state.messages to messages received.
Once connection is successful, the chained .then() will set state.socket for later use, and also use socket.send() to subscribe to the MessagesChannel. Remember once we are connected to ActionCable, we must subscribe to specific channels.
The initial subscription will start stream and do an initial broadcast, which will trigger the onmessage handler, setting the state and being rendered.
Creating A New Message and Rebroadcasting
// client/src/App.js
//... in App component
// onSubmit for form for new messagesubmitHandler = event => {
event.preventDefault();
const newMessageInfo = {
command: "message",
identifier: JSON.stringify({channel: "MessagesChannel"}),
data: JSON.stringify({action: "create_message", content: this.state.newMessage })
}
this.state.socket.send(JSON.stringify(newMessageInfo))
}# app/channel/messages_channel.rb
...
def create_message(data)
message = Message.create(content: data["content"])
messages = Message.all
ActionCable.server.broadcast("messages", {type: "current_messages", messages: messages})
end
...
The submit handler will send state.newMessage and also instructs which “action” should be invoked in the Channel. WebSocket.send() is used to send data to a WebSocket connection.
create_message action will create new message in database, and rebroadcast the updated array of messages to all subscribers.
As a result, all WebSocket connections subscribed to this channel will update their component states via the onmessage handler.
Now, our application updates the messages board real-time with any new messages posted by other consumers!
Thanks for reading, hope you found this helpful.
Any feedback is appreciated!