WebSockets in Rails are a game-changer for building real-time features. I’ve been using them a lot lately, and they’ve totally transformed how I approach web development. Let’s dive into how you can harness their power in your Rails apps.
First things first, you’ll need to set up your Rails app to use WebSockets. It’s actually pretty straightforward. Rails comes with built-in support for WebSockets through Action Cable, which is awesome. To get started, make sure you have the necessary gems in your Gemfile:
gem 'rails'
gem 'puma'
Now, let’s create a simple WebSocket channel. In your terminal, run:
rails generate channel Notifications
This will create a few files for you, including app/channels/notifications_channel.rb
. Open it up and let’s add some code:
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications_#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
end
This channel will allow us to send notifications to specific users. The subscribed
method sets up a stream for each user, and unsubscribed
cleans things up when they disconnect.
Next, we need to set up the client-side JavaScript to connect to our WebSocket. In your app/javascript/channels/notifications_channel.js
file:
import consumer from "./consumer"
consumer.subscriptions.create("NotificationsChannel", {
connected() {
console.log("Connected to notifications channel")
},
disconnected() {
console.log("Disconnected from notifications channel")
},
received(data) {
console.log("Received notification:", data)
// Handle the notification here
}
})
This sets up the client-side connection and defines how to handle incoming messages.
Now, let’s say we want to send a notification when a new comment is added to a blog post. In your CommentsController
, you might have something like this:
class CommentsController < ApplicationController
def create
@comment = Comment.new(comment_params)
if @comment.save
broadcast_notification(@comment)
redirect_to @comment.post, notice: 'Comment was successfully created.'
else
render :new
end
end
private
def broadcast_notification(comment)
post = comment.post
author = post.author
ActionCable.server.broadcast(
"notifications_#{author.id}",
{ message: "New comment on your post '#{post.title}'", comment_id: comment.id }
)
end
def comment_params
params.require(:comment).permit(:content, :post_id)
end
end
The broadcast_notification
method sends a message to the post author’s notification channel whenever a new comment is created.
But what if we want to update multiple users at once? No problem! Let’s say we have a chat room feature, and we want to broadcast new messages to all users in the room. First, let’s create a ChatRoom channel:
rails generate channel ChatRoom
In app/channels/chat_room_channel.rb
:
class ChatRoomChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_room_#{params[:room_id]}"
end
def unsubscribed
stop_all_streams
end
def speak(data)
Message.create! content: data['message'], user: current_user, chat_room_id: params[:room_id]
end
end
And in your JavaScript:
import consumer from "./consumer"
const chatRoom = consumer.subscriptions.create({ channel: "ChatRoomChannel", room_id: 1 }, {
connected() {
console.log("Connected to chat room")
},
disconnected() {
console.log("Disconnected from chat room")
},
received(data) {
console.log("Received message:", data)
// Append the message to the chat window
},
speak: function(message) {
this.perform('speak', { message: message })
}
})
// You can now call chatRoom.speak("Hello, everyone!") to send a message
Now, whenever a message is created, we need to broadcast it to all users in the room. In your Message
model:
class Message < ApplicationRecord
belongs_to :user
belongs_to :chat_room
after_create_commit { broadcast_message }
private
def broadcast_message
ActionCable.server.broadcast(
"chat_room_#{chat_room_id}",
{ message: content, user: user.username }
)
end
end
This setup will automatically broadcast new messages to all subscribers of the chat room channel.
One cool thing about WebSockets is that they’re not just for sending data from the server to the client. You can also send data from the client to the server. For example, let’s add a typing indicator to our chat room:
// In your JavaScript file
let typingTimer;
const doneTypingInterval = 1000; // ms
$('#message-input').on('keyup', function() {
clearTimeout(typingTimer);
chatRoom.perform('typing');
typingTimer = setTimeout(function() {
chatRoom.perform('stopped_typing');
}, doneTypingInterval);
});
$('#message-input').on('keydown', function() {
clearTimeout(typingTimer);
});
And in your ChatRoomChannel
:
def typing
ActionCable.server.broadcast("chat_room_#{params[:room_id]}", { typing: true, user: current_user.username })
end
def stopped_typing
ActionCable.server.broadcast("chat_room_#{params[:room_id]}", { typing: false, user: current_user.username })
end
This will send a message to all users when someone starts or stops typing.
Now, let’s talk about scaling. WebSockets can be resource-intensive, especially if you have a lot of concurrent connections. One way to handle this is by using Redis as a pub/sub adapter for Action Cable. First, add the redis gem to your Gemfile:
gem 'redis'
Then, in your config/cable.yml
:
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
This setup allows Action Cable to use Redis as a pub/sub backend, which can handle a large number of concurrent connections more efficiently.
Another important aspect of working with WebSockets is authentication. You probably don’t want just anyone to be able to connect to your WebSocket server. In your config/initializers/action_cable.rb
:
Rails.application.config.action_cable.connection_class = -> { ApplicationCable::Connection }
And in app/channels/application_cable/connection.rb
:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.signed[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
This setup will authenticate WebSocket connections using the same user sessions as your regular HTTP requests.
One thing I’ve found really useful is using WebSockets for real-time form validation. Instead of waiting for the user to submit the form, you can send each field value to the server as the user types and get instant feedback. Here’s a quick example:
// In your JavaScript
const form = consumer.subscriptions.create("FormValidationChannel", {
connected() {
console.log("Connected to form validation channel")
},
disconnected() {
console.log("Disconnected from form validation channel")
},
received(data) {
const field = document.getElementById(data.field);
if (data.valid) {
field.classList.remove('is-invalid');
field.classList.add('is-valid');
} else {
field.classList.remove('is-valid');
field.classList.add('is-invalid');
document.getElementById(`${data.field}-feedback`).textContent = data.error;
}
},
validate(field, value) {
this.perform('validate', { field: field, value: value })
}
});
document.querySelectorAll('input').forEach(input => {
input.addEventListener('input', (event) => {
form.validate(event.target.id, event.target.value);
});
});
And in your FormValidationChannel
:
class FormValidationChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
def validate(data)
result = UserValidator.new(current_user).validate_field(data['field'], data['value'])
FormValidationChannel.broadcast_to(
current_user,
{ field: data['field'], valid: result[:valid], error: result[:error] }
)
end
end
This setup will give users instant feedback as they fill out forms, which can greatly improve the user experience.
WebSockets can also be super useful for building collaborative features. Imagine you’re building a collaborative document editor. You could use WebSockets to sync changes between users in real-time. Here’s a basic implementation:
# In your DocumentsChannel
class DocumentsChannel < ApplicationCable::Channel
def subscribed
@document = Document.find(params[:id])
stream_for @document
end
def receive(data)
DocumentsChannel.broadcast_to(@document, data)
end
end
// In your JavaScript
const docId = document.getElementById('editor').dataset.documentId;
const editor = document.getElementById('editor');
const doc = consumer.subscriptions.create({ channel: "DocumentsChannel", id: docId }, {
received(data) {
if (data.user_id !== currentUserId) {
editor.value = data.content;
}
}
});
editor.addEventListener('input', (event) => {
doc.send({ content: event.target.value, user_id: currentUserId });
});
This simple setup allows multiple users to edit the same document simultaneously, with changes synced in real-time.
One last thing I want to mention is error handling. When working with WebSockets, it’s important to handle disconnections gracefully. On the client side:
consumer.subscriptions.create("SomeChannel", {
// ...other methods...
disconnected() {
console.log("Disconnected. Attempting to reconnect...");
setTimeout(() => {
consumer.connect();
}, 3000);
}
});
This will attempt to reconnect if the connection is lost. On the server side, you might want to clean up any resources associated with the connection:
class SomeChannel < ApplicationCable::Channel
def subscribed
# Set up connection
end
def unsubscribed
# Clean up resources
end
end
WebSockets have opened up a whole new world of possibilities for web development. They allow us to create truly interactive, real-time applications that were previously difficult or impossible to build. Whether you’re creating a chat application, a collaborative tool, or just want to add some real-time flair to your existing app, WebSockets and Action Cable in Rails provide a powerful and flexible solution.
Remember, while WebSockets are awesome, they’re not always the best solution for every problem. Sometimes good old HTTP requests are still the way to go. As with all tools in programming, it’s about using the right tool for the job. But when you need real-time, bidirectional communication between the client and server, WebSockets are hard to beat.
I hope this deep dive into WebSockets and Action Cable in Rails has been helpful. It’s a topic I’m really passionate about, and I’ve had a lot of fun exploring all the cool things you can do with this technology. Happy coding