Real Time Page Updates with Rails and Hotwire - Turbo Broadcasts

Hotwire has been the default frontend framework for Rails Application since Rails 7. And one of the most important framework within Hotwire is Turbo which uses multiple techniques to provide a SPA experience within our application.
And one of the things I really like about Turbo is the ability to provide real time page updates quickly and easily and without having to write any javascript code with it.
In this example, Let’s say we have an Event app where you can register to, And we will apply real time page updates on any modifications to the Events table or whenever someones registers for an event. Below with be the end result we would want to achieve
Turbo Broadcast
Turbo Broadcast allows us to broadcast messages via Websockets to multiple clients in real-time and which is what we will be using in this example. This is the source code for Turbo Broadcast and its worth taking a look at because it provides some example usages in the inline comments https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb
So lets say we have an existing table of upcoming events, The first thing we need to do is inject this line turbo_stream_from “events” within the html file that renders this table, and add an ID to the HTML element that contains the data and HTML elements we would want to update in real-time
< main >
< %= turbo_stream_from "events" % > < !-- Add this line !-- >
< h3 class="header mb-4" > Upcoming Events </h3 >
< table class="table" >
< thead >
< th > Event Name < /th >
...
< /thead >
< tbody id='eventsTable' > < !-- Assign an ID !-- >
< % @events.each do |event| % >
< %= render partial: "event", locals: { event: event } % >
< % end % >
< /tbody >
< /table >
< /main >
What this do is establishes a websocket connection on that page to subscribes users to that channel. That helper method would produce something like this, where signed-stream-name is the signed version of the passed string “events”
< turbo-cable-stream-source
channel="Turbo::StreamsChannel"
signed-stream-name="signed-version-of-passed-string" >
< /turbo-cable-stream-source >
So in this context, All of the users currently in that events page are subscribed to the Turbo::StreamsChannel and waiting for broadcasts that will be made on the events stream
Also, on the _event.html.erb partial, we needd to add an ID per each event row
# app/views/events/_event.html.erb
< tr id="< %= dom_id event % >" >
< td > < %= event.name % > < /td >
...
< /tr >
dom_id is a Rails helper that will return a string of the model name and ID e.g event_1
Now that we have that turbo stream setup and added the IDs that we needded, we need to add 3 lines of active record callbacks to the Event model
class Event < ApplicationRecord
include ActionView::RecordIdentifier
has_many :bookings
after_create_commit { broadcast_prepend_to "events", target: "eventsTable" }
after_update_commit { broadcast_replace_to 'events', target: dom_id(self) }
after_destroy_commit { broadcast_remove_to 'events', target: dom_id(self) }
end
To explain further on, The broadcast method’s first argument is the stream_name which is events coming from the stream name we’ve passed in <%= turbo_stream_from “events” %>
The target parameter is the ID of the HTML element we would want to be modified. So you can notice that on create, We would want to modify the Table Body which we defined the ID as eventsTable.
And of course on update and destroy, we will modify the actual table row that the event is rendered to.
It also accepts a parameter called partial, But we don’t need to add it in here. The naming convention that Turbo Broadcasts maps to by default will be based on the Model name. So in our case the Event Model, Turbo will then try to find a partial /events/_event if the partial parameter has not beed provided.
A thing to note, We need to include ActionView::RecordIdentifier so that we could use the dom_id helper inside the Model class. And thats it! With these few lines of code, The Events page will receive real time updates given any modification, addition or deletion in the Events table.
But this only covers any changes on the Event table, We need to be able to update the events page whenever a booking is created.
There are two options, First, we can add touch: true on that Booking model
class Booking < ApplicationRecord
belongs_to :event, touch: true
end
This will update the associated event’s updated_at timestamp whenever a booking is created. But often times that not, This is not the behavior we intend to, So we can just define an active record callback as well to this model
class Event < ApplicationRecordd
...
after_update_commit { broadcast_updates! }
def broadcast_updates!
broadcast_prepend_to "events", target: "eventsTable"
end
end
class Booking < ApplicationRecord
belongs_to :event
after_create_commit { event.broadcast_updates! }
end
We define a reusable instance method for broadcasting update changes so that we can define it both on the Event and Booking Model. And now we have real time page updates whenever someone registers for an event
Adding Loading and Transitions on broadcasts
We have setup real time page updates on the events page, But ideally we want to be able to improve the user experience by adding loading and transitions whenever something changes on the events page. We can do that by adding and updating a few lines of code.
First thing, we need to add another event partial that will render a loading row, In this context, Im using Bootstrap spinner for simplicity.
# app/views/event/_loading_event.html
< tr id= "<%= dom_id event % >" >
< td> < div class="spinner-border" role="status" > < /td >
< % 4.times do % >
< td > ... < /td >
< % end % >
< /tr >
Which would look like this
Then we would want to add a simple in out transition css class, and allow the event partial to receive an optional transition_class parameter
# CSS
.in-out {
animation: fade-in 0.5s ease-out,
slide-in 0.5s ease-out;
}
# app/views/events/_event.html.erb
< % transition_class ||= nil % >
< tr id="< %= dom_id event % >" class="< %= transition_class % >" >
...
< /tr >
Then in the Events Model, We would want to change our callbacks
class Event < ApplicationRecord
...
after_create_commit { broadcast_create! }
after_update_commit { broadcast_updates! }
after_destroy_commit { broadcast_remove_to 'events', target: dom_id(self) }
def broadcast_create!
broadcast_prepend_to "events", target: "eventsTable", partial: "/events/loading_event"
sleep(0.5)
broadcast_replace_to 'events', target: dom_id(self), locals: { event: self, transition_class: "in-out" }
end
def broadcast_updates!
broadcast_replace_to 'events', target: dom_id(self) , partial: "/events/loading_event"
sleep(0.5)
broadcast_replace_to 'events', target: dom_id(self), locals: { event: self, transition_class: "in-out" }
end
end
- To Load the loading event partial
Notice here, That we explicitly passed the partial argument, By default, The Turbo Broadcast will find a partial based on the model name e.g if the model name is Event, it would look for /app/views/events/_event.html. That is the reason we didn’t need to pass the partial argument previously.
- And after some delay, replace the loading event partial with the actual event partial with updated data
And notice that we are passing the in_out transition class to give an transition effect when the turbo stream renders the updated element
And the end result with would be something like this
And that’s a wrap! Thanks to Turbo, With a few lines of code, We can implement real time page updates on our application with a few lines of code.
Ps. if you have any questions
Ask here