Heading image for post: Broadcasting User Specific Turbo Content

Ruby

Broadcasting User Specific Turbo Content

Profile picture of Andrew Vogel Profile picture of Matt Polito

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.

Screenshot 2025-01-25 at 2.00.17 PM

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 %>

Screenshot 2025-01-25 at 2.03.08 PM

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.

Imgur

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.

  1. We can broadcast an update that contains a lazy Turbo frame, which will have the context for each connected client.
  2. 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?

  1. When the broadcast happens, we replace the appointment with our lazy turbo frame.
  2. Turbo loads our appointment partial via the frame
  3. The appointment turbo frame is updated with the latest content

Blog Gif - Turbo Streams (Frames) with Context

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.

Blog Gif - Turbo Broadcast with Morph

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!

Photo by Jon Flobrant - Unsplash

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project