Ruby
Broadcasting User Specific Turbo Content
A fantastic feature of Turbo Rails is the ability to broadcast
changes to a page. Each connected client can receive changes in real-time. However, it's a common problem to update content that is shown to many users, but the rendering of items is affected by user context. A recommendation to get around this is to only broadcast things that don't require that user context. Because we have Turbo, we can serve user-specific content in other ways.
Let's examine our setup, and I'll explain how we can accomplish our goal.
Premise
We have a scheduling dashboard with appointments, and all users of this system can view it. If one of the appointments is updated by another user, we want to "broadcast" those changes to every user viewing the dashboard. We only want users who created the appointment to be able to edit it. Now we see the problem: broadcasting updates doesn't have the user context. We don't know who gets to see the Edit link.
The Basics of Streaming
How do you broadcast an update?
class Appointment < ApplicationRecord
after_save_commit -> {
broadcast_replace_to(
:appointments_dashboard,
self,
partial: "appointments/appointment",
locals: { appointment: self }
)
}
end
Then we need our partial app/views/appointments/_appointment.html.erb
to render the appointment -
erb
<%= turbo_frame_tag(appointment) do %>
<% # ...content %>
<% end %>
And we have our "dashboard", the index page for the appointments -
<%= turbo_stream_from "appointments_dashboard" %>
<%= render appointments, as: :appointment %>
Digging into the Context Problem
We have a basic partial that we want to render on the appointments index. But now we want to change it only to show an "Edit" action if the user created the appointment. With this in mind, let's review our requirements -
- This index is viewable by all users of our system
- Each appointment has "permissions" around actions that can be taken
- Every user can view an appointment
- Only the user that creates the appointment can see the link to edit that appointment
Using the User Object in our Partial
Let's change the appointment partial - we want to display the edit link only if the current_user
created the appointment.
In our view, we can easily do that by wrapping the link in an if statement
<% if appointment.created_by?(Current.user) %>
<%= link_to "Edit", edit_appointment_path(appointment) %>
<% end %>
But now, when an update happens to an appointment, and our rails server streams an update, the Current.user
will be nil - our server doesn't have the context of each connection. None of our streamed updates will render an Edit link - even if one of the connected users should have the link.
Managing Global Context
How can we fix this?
Like anything in software, there are many ways to solve a problem; let's review a couple.
- We can broadcast an update that contains a lazy Turbo frame, which will have the context for each connected client.
- We can use Turbo's
morph
feature to broadcast a refresh to our appointment list
1. Broadcast an Update with a Lazy Loading Turbo Frame
Let's add a route that will be the src
for our lazy-loaded turbo frame -
resources :appointments, only: [:index, :edit, :update] do
member do
get :row
end
end
We'll add our template for app/views/appointments/row.html.erb
. This will render our appointment partial -
<%= render "appointment", appointment: %>
Penultimately, we add a new partial to use in our model's broadcast method. We put this at app/views/appointments/_load_row.html.erb
-
<%= turbo_frame_tag(appointment, src: row_appointment_path(appointment)) %>
Finally, we are going to use this in our broadcast -
class Appointment < ApplicationRecord
# ...
after_save_commit -> {
broadcast_replace_to(
"appointments_list",
partial: "appointments/load_row",
locals: { appointment: self }
)
}
# ...
end
What's going on here?
- When the broadcast happens, we replace the appointment with our lazy turbo frame.
- Turbo loads our appointment partial via the frame
- The appointment turbo frame is updated with the latest content
As you can see from the gif, there is a slight flicker when the appointment is lazy loading. Because our turbo frame has no content while it loads, we should introduce a spinner or some skeleton to show it's loading. But our request payload is small and only loads the appointment to get the changes.
The code for this solution is available on the main
branch of the example project.
2. Broadcast a Refresh
To use Turbo Morph, we'll need to add some meta tags to our application layout. These tags enable the morph feature and preserve the scroll position.
In your app/views/layouts/application.html.erb
, add the following metatags to the <head>
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">
Then, we can change the broadcast mechanism in our appointment model to the following -
class Appointment < ApplicationRecord
#...
after_save_commit -> {
broadcast_refresh_to("appointments_list")
}
# ...
end
Let's update an appointment in the console and see if all connected clients receive the update with the proper rendering of the "Edit" link.
Now you can see that all clients have received the refresh that we expected.
There is a caveat with using this method to get the latest content. Under the hood, Turbo is doing a full reload and diffing the page content. For this trivial example, we are ok, but if you do this for a page with many records, you may experience some performance issues. Keep that in mind when designing a page.
The code for this solution is available on the turbo-morph
branch of the example project.
Wrapping Up
As you saw, we can use morph
or lazy turbo frames to refresh user-specific content with broadcasting. Turbo is a flexible tool you can leverage to build reactive applications. If you have questions or need other solutions, feel free to reach out. Happy Hacking!