#17 Turbo Streams: Broadcasts
    
  
  
    
    
  Turbo Streams in Controller VS Broadcasts: When to use which?
Rule of thumb:
- -> If you want to send updates to a page when a user INTERACTS with the page (clicks something) -> HTTP Turbo Streams
 - -> If you want to send updates to a page WITHOUT user interaction -> Websocket Turbo Stream Broadcasts
 
I would use broadcasts for:
- live chat
 - “async” notification
 - live dashboards
 
I would NOT use broadcasts for:
- user-triggered CRUD updates (like a post, add a comment, edit a record, search)
 
If you look at the docs for turbo broadcasts, the suggested way to trigger them are ActiveRecordCallbacks in a model.
Callbacks to use:
after_create_commitafter_update_commit- 
after_destroy_commit(btw,deletedoesn’t fire a callback.destroydoes) after_save_commit
- 
append# add on bottom of DOM ID (<div id="abc">) - 
prepend# add on top of DOM ID - 
replace# replace a DOM ID (example: with an element with another id) - 
update# update content INSIDE a DOM ID - 
remove# no template required for this one! - 
before# add before DOM ID (not inside it)! - 
after# add after DOM ID (not inside it)! 
1. Broadcast Create/Update/Destroy #
- Initial setup:
 
rails g scaffold inbox name
rails db:migrate
bundle add faker
- Add a 
turbo_stream_fromtarget with an ID anywhere on a page. 
# app/views/inboxes/index.html.erb
++<%= turbo_stream_from "inbox_list" %>
  <div id="inboxes">
    <%= render @inboxes %>
  </div>
- This will “listen” to broadcasts, with a target 
inbox_list - Now, when you navigate to a page that has 
turbo_stream_from, you will see something like this in the console: 

- Next, add a broadcasts, with a target 
inbox_listin the model: 
# app/models/inbox.rb
class Inbox < ApplicationRecord
  validates :name, presence: true, uniqueness: true
++broadcasts_to ->(inbox) { :inbox_list }
end
The above
- requires 
<%= turbo_stream_from :inbox_list %> - a default target - 
<div id="inboxes"> - 
    
a default partial -
"inboxes/_inbox" - 
    
This will let you broadcast all activity (create, update, destroy).
 - That’s it! Now, you can try to create/update/destroy records in the console or in another tab:
 
Inbox.create(name: Faker::Quote.famous_last_words)
Inbox.first.update(name: "Edited at #{Time.zone.now}")
Inbox.first.destroy
- … and changes will be “broadcasted” without page refresh:
 

  
  
    2. broadcasts_to is too magical. Let’s unbuild it! #
  
  
    
- 
broadcasts_to ->(inbox) { :inbox_list }translates to: 
# app/models/inbox.rb
--broadcasts_to ->(inbox) { :inbox_list }
++
++broadcasts_to ->(inbox) { :inbox_list }, inserts_by: :append, target: 'inboxes'
- or, more precisely
 
# app/models/inbox.rb
class Inbox < ApplicationRecord
--broadcasts_to ->(inbox) { :inbox_list }
--
--broadcasts_to ->(inbox) { :inbox_list }, inserts_by: :append, target: 'inboxes'
++
++after_create_commit { broadcast_append_to "inbox_list" }
++after_update_commit { broadcast_replace_to "inbox_list" }
++after_destroy_commit { broadcast_remove_to "inbox_list" }
++
++# after_create_commit { broadcast_prepend_to "inbox_list" } # would add on top
++# after_update_commit { broadcast_update_to "inbox_list" } # would add dom_id(inbox) inside dom_id(inbox)
end
- or, even more precisely:
 
# app/models/inbox.rb
class Inbox < ApplicationRecord
--broadcasts_to ->(inbox) { :inbox_list }
--
--broadcasts_to ->(inbox) { :inbox_list }, inserts_by: :append, target: 'inboxes'
--
--after_create_commit { broadcast_append_to "inbox_list" }
--after_update_commit { broadcast_replace_to "inbox_list" }
--after_destroy_commit { broadcast_remove_to "inbox_list" }
--
--# after_create_commit { broadcast_prepend_to "inbox_list" } # would add on top
--# after_update_commit { broadcast_update_to "inbox_list" } # would add dom_id(inbox) inside dom_id(inbox)
++
++after_create_commit do
++  broadcast_append_to('inbox_list', target: 'inboxes', partial: "inboxes/inbox", locals: { inbox: self })
++end
++
++after_update_commit do
++  broadcast_replace_to('inbox_list', target: self, partial: "inboxes/inbox", locals: { inbox: self })
++end
++
++after_destroy_commit do
++  broadcast_remove_to('inbox_list', target: self)
++end
end
So, in a turbo_stream you can specify:
- a 
turbo_stream_frombroadcast (connection ID) to listen to - a target - HTML element with an ID 
DOM ID(<div id="abc">) that gets replaced/updated/appended/destroyed… with a partial or HTML - a partial/html to stream… for which you can set locals
 - locals - local variables
 
I recommend to use explicity paths. No shortcut magic!
  
  
    3. Broadcast HTML: Update inboxes count on create/destroy. #
  
  
    
- add a 
div idin the view that will be updated by the broadcast. - add a 
turbo_stream_from. You can use the same stream from above. 
# app/views/inboxes/index.html.erb
<%= turbo_stream_from "inbox_list" %>
<div id="inbox_count">
  <%= @inboxes.count %>
</div>
- send some HTML to the target 
div idwhen an inbox is created/destroyed - set a matching turbo stream ID in the view and controller (
inbox_list) 
# app/models/inbox.rb
  after_commit :send_html_counter, on: [ :create, :destroy ]
  def send_html_counter
    broadcast_update_to('inbox_list', target: 'inbox_count', html: "There are #{Inbox.count} inboxes")
    # broadcast_update_to('inbox_list', target: 'inbox_count', html: Inbox.count)
  end
Now, you can create/destroy a record in the rails console and the counter will be updated!
  
  
    4. Broadcast Partial: Update inboxes count on create/destroy. #
  
  
    
- create the partial:
 
# app/views/inboxes/_inbox_count.html.erb
Total inboxes:
<%= inbox_q %>
- display it in a view:
 
# app/views/inboxes/index.html.erb
<%= turbo_stream_from "inbox_list" %>
<div id="inbox_count">
  <%= render partial: "inboxes/inbox_count", locals: {inbox_q: Inbox.count} %>
</div>
- add a broadcast to update the content within 
<div id="inbox_count"> 
# app/models/inbox.rb
  after_commit :send_partial_counter, on: [ :create, :destroy ]
  def send_partial_counter
    broadcast_update_to('inbox_list', target: 'inbox_count', partial: "inboxes/inbox_count", locals: { inbox_q: Inbox.count })
  end
Surely, you can also send a partial without locals ;)
  
  
    5. Error broadcasting button_to #
  
  
    
Before rails 7.0.0rc you might have a CSFR error when streaming button_to:
`ensure_session_is_enabled!': Request forgery protection requires a working session store but your application has sessions disabled. You need to either disable request forgery protection, or configure a working session store. (ActionController::RequestForgeryProtection::DisabledSessionError)
For example, in a case like this:
# app/views/inboxes/_inbox.html.erb
<div id="<%= dom_id inbox %>">
  <%= inbox.id %>
  <%= inbox.name %>
++<%= button_to "Destroy this inbox", inbox_path(inbox), method: :delete %>
</div>
It can be fixed by adding:
# config/application.rb
++ config.action_controller.silence_disabled_session_errors = true
6. Broadcasting ViewComponent #
Previously I wrote about (4 ways to Turbo Stream ViewComponent). They won’t work from a model.
However, there is a way:
- Install ViewComponent
 - Create a component
 
bundle add view_component
rails g component inbox inbox
# app/components/inbox_component.html.erb
<div id="<%= dom_id inbox %>">
  <%= inbox.name %>
  <%= link_to "Show this inbox", inbox %>
  <%= link_to "Edit this inbox", edit_inbox_path(inbox) %>
  <%= button_to "Destroy this inbox", inbox_path(inbox), method: :delete %>
</div>
- add 
attr_reader :inboxto be able to accessinboxwithout@inbox 
# app/components/inbox_component.rb
class InboxComponent < ViewComponent::Base
  attr_reader :inbox
  def initialize(inbox:)
    @inbox = inbox
  end
end
- now you can render a single inbox or a collection like this:
 
<%= render(InboxComponent.with_collection(@inboxes)) %>
<%= render InboxComponent.new(inbox: Inbox.first) %>
- so, render the component(s) in the view:
 
# app/views/inboxes/_inbox.html.erb
<%= turbo_stream_from "inbox_list" %>
<div id="inboxes">
  <%= render(InboxComponent.with_collection(@inboxes)) %>
  <%#= render @inboxes %>
</div>
- and broadcast them in the model LIKE THIS
 
# app/models/inbox.rb
  after_create_commit do
    # these will not render the HTML
    # InboxComponent.new(inbox: self)
    # render_to_string(InboxComponent.new(inbox: self))
    # view_context.render(InboxComponent.new(inbox: self))
    # InboxComponent.new(inbox: self).render_in(view_context)
    # this will:
    broadcast_append_to('inbox_list', target: 'inboxes', html: ApplicationController.render(InboxComponent.new(inbox: self)))
  end
7. Broadcasting associations #
- Add 
messagestoinboxes 
rails g scaffold message body:text inbox:references
# app/models/inbox.rb
  has_many :messages
# app/models/message.rb
  belongs_to :inbox
- render messsages inside an inbox
 - add a 
turbo_stream_fromtarget that is UNIQUE for this inbox 
# app/views/inboxes/show.html.erb
<%= render @inbox %>
<%= turbo_stream_from @inbox, :messages %>
<div id="<%= dom_id(@inbox, :messages) %>">
  <%= render @inbox.messages %>
</div>
- Now, broadcast messages into an inbox
 - 
[inbox, :messages]will stream todom_id(@inbox, :messages) 
# app/models/message.rb
class Message < ApplicationRecord
  belongs_to :inbox
  # lets you use dom_id in a model
  include ActionView::RecordIdentifier
  after_create_commit do 
    broadcast_prepend_to [inbox, :messages], target: dom_id(inbox, :messages), partial: "messages/message", locals: { message: self }
    # broadcast_prepend_to [inbox, :messages], target: ActionView::RecordIdentifier.dom_id(inbox, :messages)
  end
  after_update_commit do
    broadcast_update_to [inbox, :messages], target: self, partial: "messages/message", locals: { message: self }
  end
  after_destroy_commit do
    broadcast_remove_to [inbox, :messages], target: self
  end
end
- Now you can add messages to an inbox and they will be broadcasted into the inbox!
 
Inbox.first.messages.create body: SecureRandom.hex
Inbox.first.messages.last.update body: "hello world"
Inbox.first.messages.last.destroy
8. Best practices when broadcasting #
It is never recommended to use callbacks in a model.
I highly recommend to trigger broadcasts in controller actions instead.
This way, your code will be more predictable and reliable.
# app/controllers/messages_controller.rb
def destroy
  Turbo::StreamsChannel.broadcast_update_to([inbox, :messages],
                                            target: @message,
                                            partial: "messages/message",
                                            locals: { message: @message })
  Turbo::StreamsChannel.broadcast_update_to('global_notifications',
                                            target: 'flash',
                                            partial: "shared/flash",
                                            locals: { flash: flash })
end
  
  
    P.S. WTF dom_id?! #
  
  
    
Here’s how ActionView::RecordIdentifier dom_id works:
# dom_id(Inbox.first)
# => inbox_1
# dom_id(Inbox.first, :hello)
# => hello_inbox_1
That’s it!
Official Turbo/Broadcastable docs
Next, I hope to explore Broadcasts + Devise + Authorization
Did you like this article? Did it save you some time?