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.PremiseWe 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 StreamingHow 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 ProblemWe 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 systemEach appointment has "permissions" around actions that can be takenEvery user can view an appointmentOnly the user that creates the appointment can see the link to edit that appointmentUsing the User Object in our PartialLet'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 ContextHow 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 list1. Broadcast an Update with a Lazy Loading Turbo FrameLet'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 frameThe appointment turbo frame is updated with the latest contentAs 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 RefreshTo 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 UpAs 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!Photo by Jon Flobrant - Unsplash