DEV Community

Mike Rogers ✈️
Mike Rogers ✈️

Posted on

Real-Time Partial Updates in Ruby on Rails using hotwire-rails

The hotwire-rails was just released today by Basecamp. It's a collection of JavaScript & Ruby libaries which allows for making some pretty rich user experiences.

The big thing I like, was how easy it is to create real-time partials in Ruby on Rails.

Now we just the need code:

# app/models/post.rb
class Post < ApplicationRecord
  # We're going to publish to the stream :posts
  # It'll update the element with the ID of the dom_id for this object,
  # Or append our posts/_post partial, to the #posts <tbody>
  broadcasts_to ->(post) { :posts }
end
Enter fullscreen mode Exit fullscreen mode

Then within the views, having:

<!-- app/views/posts/index.html.erb -->
<%= turbo_stream_from :posts %>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Body</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody id="posts">
    <%= render @posts %>
  </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode
<!-- app/views/posts/_post.html.erb -->
<tr id="<%= dom_id post %>">
  <td><%= post.title %></td>
  <td><%= post.body %></td>
  <td><%= link_to 'Show', post %></td>
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
  <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
Enter fullscreen mode Exit fullscreen mode

Top comments (14)

Collapse
 
rusty_sys_dev profile image
Scott

In a manner of speaking I assume that the turbo_stream_from subscribes to the Posts ApplicationRecord's broadcast_to?

Additionally is @posts available to the view because of the broadcast, or are you just not showing your method implementations?

Collapse
 
mikerogers0 profile image
Mike Rogers ✈️

In a manner of speaking I assume that the turbo_stream_from subscribes to the Posts ApplicationRecord's broadcast_to?

Close! Theturbo_stream_from :posts tells the users Browser to subscribe to :posts stream (It just adds a snippet of HTML to the page) & in the Post model we're telling it to broadcast to the:posts stream.

Additionally is @posts available to the view because of the broadcast,

I didn't post up the code controller action where I set that, I'll aim to do a more in depth write up in the future. The broadcast method passes the current model to app/views/posts/_post.html.erb as the post value.

Collapse
 
rusty_sys_dev profile image
Scott

Thanks! That clarifies things a lot!

Ill keep my fingers crossed and an eye out for your detailed write up!

Collapse
 
almokhtar profile image
almokhtar bekkour

thanks for sharing, just small question If it uses websockets to update the page, does that mean you’re persisting a connection with every user at all times when they’re using the app? Sounds expensive to scale compared to a SPA + JSON APIs.

Collapse
 
mikerogers0 profile image
Mike Rogers ✈️

If it uses websockets to update the page, does that mean you’re persisting a connection with every user at all times when they’re using the app

That's correct. It's a little expensive, but you'll be able to push down changes from the server the moment the stuff changes as opposed to having the user poll for changes from a JSON API.

The key win here is I can get the read-time updates, without having to write any JavaScript & worry about maintaining state.

Collapse
 
staleo profile image
Stas Syritsyn

Hey Mike, thx for your efforts and a nice lesson!
Got a question tho. Imagine we need to broadcast from the controller action, not from the model with a proc.
Here's how I do it:

respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.prepend(:tweets, partial: "tweets/tweet",
locals: { tweet: @tweet })
end
format.html { redirect_to tweets_url, notice: 'Tweet was successfully created.' }
format.json { render :show, status: :created, location: @tweet }
end

This approach works pretty much the same, but the new tweets don't appear in another browser tab! New tweet does show up on the author's tab, but on any other tab/browser/device -- it simply doesn't.
Frankly, I didn't really spend much time digging in the docs, did I miss anything maybe?
Best.

Collapse
 
mikerogers0 profile image
Mike Rogers ✈️

turbo_stream is like an AJAX update to the user who made that request. Personally I'm going to stay far away from it & stick with using broadcast_to with my model.

Collapse
 
sidali profile image
Sid Ali BENTIFRAOUINE

Well done m8!

Collapse
 
anthonyfrancis profile image
Anthony Francis • Edited

Can Hotwire turbo_stream two different streams from the same object on the same page?

Tried copying the frame code twice in the view but only one updates live.

Collapse
 
roberthopman profile image
Robert

yes, that's possible.

Collapse
 
anthonyfrancis profile image
Anthony Francis • Edited

Is Hotwire able to broadcast to two different model views? I'm having some trouble with that :/

Collapse
 
mikerogers0 profile image
Mike Rogers ✈️ • Edited

Yeah it can! Post your code up :)

Collapse
 
anthonyfrancis profile image
Anthony Francis

So I've figured out how to broadcast/render to two different model views, but I'm trying to broadcast/render a different partial/design for the view.

Whenever Hotwire broadcast/renders an object, it looks for the original partial, in my case questions/question through id="questions".

When broadcasting a new object to a view, the object gets broadcasted from questions/question and only renders the desired partial questions/question_live once the page has been refreshed, or if the questions/question_live is already on the page.

Example code

<tbody id="questions">
<%= render collection: @questions, partial: "questions/question_live", as: :question %>
</tbody>

Do you know if there is a way to stop Hotwire from rendering the id="questions" from partial questions/question and render from questions/question_live?

Thread Thread
 
anthonyfrancis profile image
Anthony Francis

Figured it out.

Question model

broadcasts_to ->(question) { :rooms }
after_create_commit ->(question) { broadcast_append_to :live_rooms, partial: "questions/question_live" }
after_update_commit ->(question) { broadcast_replace_to :live_rooms, partial: "questions/question_live }
after_destroy_commit ->(question) { broadcast_remove_to :live_rooms }
Enter fullscreen mode Exit fullscreen mode

live_room show view
<%= turbo_stream_from :live_rooms %>
...
code...
...

room show view
<%= turbo_stream_from :rooms %>
...
code...
...