Turbo streams broadcasting in Rails, update methods

Turbo streams broadcasting in Rails, update methods

Rails’ turbo streams broadcasts let you update your UI in real time using a range of handy methods. In this series, I’ll break down these methods in pairs—highlighting their similarities and key differences that can significantly impact data consistency and performance.

For example, today’s post compares broadcast_update_to with broadcast_update_later_to. How do they work? What’s similar between them, and what sets them apart? When should you pick one over the other? I’m answering these questions for you and for my future self when I inevitably search for a quick refresher. Sound familiar? ✋🏼

Before we dive into the specifics, let’s cover the core concept behind these methods and the key factors affecting data consistency and performance. This foundation will help you decide which method fits best in different scenarios.

The Immediate vs. Delayed Paradigm

  • Immediate Methods (e.g. broadcast_update_to) send the update right away. They’re great when you’re not inside a database transaction or need instant results. However, if used inside a transaction, they might run before the changes are fully saved, potentially broadcasting incomplete data.

  • Delayed Methods (e.g. broadcast_update_later_to) wait until the transaction commits (using ActiveJob) before sending the update. This slight delay ensures that only final, committed data is broadcast, helping you avoid race conditions and making your production app more reliable.

Update Methods

broadcast_update_to

  • What: Sends an update action immediately to the specified target.

  • Risks: May broadcast uncommitted data if used inside a transaction.

  • Performance: Runs synchronously, which can block the main thread.

  • Usage Note: Avoid using this method within a transaction.

Immediate update example:

Turbo::StreamsChannel.broadcast_update_to(
  "posts",
  target: "post_#{post.id}",
  partial: "posts/post",
  locals: { post: post }
)

Controller example – immediate update (⚠️ Not recommended within a transaction):

class NotificationsController < ApplicationController
  def update_notification_immediate
    Notification.transaction do
      @notification = Notification.find(params[:id])
      @notification.update!(message: "Immediate update!")

      # Immediate broadcast: may send uncommitted data if used inside a transaction.
      Turbo::StreamsChannel.broadcast_update_to(
        "notifications",
        target: "notification_#{@notification.id}",
        partial: "notifications/notification",
        locals: { notification: @notification }
      )
    end

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to notifications_path }
    end
  end
end

broadcast_update_later_to

  • What: Schedules an update action to occur only after the transaction is committed.

  • Benefits: Ensures that only fully committed and consistent data is broadcast.

  • Performance: Runs asynchronously, so your app continues processing without waiting.

  • Tip: Use this method when updating records within a transaction (e.g., after saving) to ensure data consistency. Make sure your ActiveJob adapter (like Redis) is properly set up in production.

Delayed update example:

ActiveRecord::Base.transaction do
  post.save!
  Turbo::StreamsChannel.broadcast_update_later_to(
    "posts",
    target: "post_#{post.id}",
    partial: "posts/post",
    locals: { post: post }
  )
end

Controller example – delayed update:

class NotificationsController < ApplicationController
  def update_notification_delayed
    Notification.transaction do
      @notification = Notification.find(params[:id])
      @notification.update!(message: "Delayed update!")

      # Delayed broadcast: fires after the transaction commits,
      # ensuring only committed data is broadcast.
      Turbo::StreamsChannel.broadcast_update_later_to(
        "notifications",
        target: "notification_#{@notification.id}",
        partial: "notifications/notification",
        locals: { notification: @notification }
      )
    end

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to notifications_path }
    end
  end
end

Model level vs. Controller broadcasting

There are two common approaches for broadcasting updates:

Controller Broadcasting:
You manually trigger broadcasts in your controller actions.

  • Pros: Fine-grained control over each action.

  • Cons: Can lead to duplicated code and requires manual transaction management.

Model Broadcasting (Callbacks):
You handle broadcasts inside model callbacks (e.g. using after_update_commit), which means broadcasts occur automatically whenever the model updates.

  • Pros: Keeps broadcast logic encapsulated, resulting in cleaner controllers and safer broadcasts (only after the transaction commits).

  • Cons: Less granular control in the controller; broadcasts happen on every update unless conditionally suppressed.

Model-level broadcasting example:

class Notification < ApplicationRecord
  include Turbo::Broadcastable

  # Automatically broadcasts a delayed update when the record is updated.
  after_update_commit :broadcast_notification_update

  private

  def broadcast_notification_update
    broadcast_update_later_to(
      "notifications",
      target: dom_id(self),  # Using Rails' dom_id helper for unique targeting
      partial: "notifications/notification",
      locals: { notification: self }
    )
  end
end

Controller example when using model-level broadcasting:

class NotificationsController < ApplicationController
  def update
    @notification = Notification.find(params[:id])
    if @notification.update(notification_params)
      # No explicit broadcast needed here; it's handled by the model callback.
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to notifications_path }
      end
    else
      # Handle validation errors.
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def notification_params
    params.require(:notification).permit(:message, :user_id)
  end
end

Both approaches (model and controller) work well—it really depends on your project's needs. Controller broadcasting offers granular control, while model broadcasting encapsulates logic and ensures safety by only broadcasting committed data.

Wrapping up

That’s it for today’s deep dive into update methods in turbo streams! We explored:

  • The differences between immediate broadcast_update_to and delayed broadcast_update_later_to updates.

  • Practical examples at both the controller and model level.

Next, I’ll break down another Turbo Streams method, perhaps the remove methods.

Want to learn more, you must check the following turbo stream and all Rails educational resources:

If you found this helpful and want more hands-on examples, subscribe for updates. Your future self (and your code) will thank you!

Thanks for reading, and happy broadcasting, may your streams always be smooth (and your bugs few) !

Did you find this article valuable?

Support Ahmed Nadar by becoming a sponsor. Any amount is appreciated!