Tools: Hotwire Decoded: When to use Frames vs. Streams

Tools: Hotwire Decoded: When to use Frames vs. Streams

Source: Dev.to

The Hotwire Confusion ## πŸ–ΌοΈ Turbo Frames: "The Room" ## Use Case: Inline Editing ## 🌊 Turbo Streams: "The Puppet Master" ## Use Case: Live Chat / Real-time Feeds ## The Cheat Sheet (Visual Decision Matrix) ## Scenario A: Search Filter ## Scenario B: Shopping Cart ## Scenario C: Infinite Scroll ## Summary If you are new to Rails 7 or 8, you’ve likely stared at the documentation asking: "Both of these update the DOM without a page reload... so which one do I use?" It is the most common point of confusion. Here is the visual guide to making the right choice. Visual Mental Model: Imagine your webpage is a house. A Turbo Frame is a specific room with the door closed. Rule: "What happens inside the frame, stays inside the frame." The classic example. You have a list of messages. You want to edit one without reloading the list. The View (_message.html.erb): The Result: The show version of the message is replaced by the edit form version of the message. The rest of the page (header, footer, sidebar) never moves. Visual Mental Model: Imagine a Puppet Master pulling strings. Rule: "One action, multiple targets." You submit a new message. You can't use a Frame here because the input form is at the bottom of the page, but the new message needs to appear in the list above it. The frame rules would break. The View (create.turbo_stream.erb): You type in a search box, and the list below updates. πŸ‘‰ Use a Frame. wrap the input and the list in one Frame. The search acts as a navigation event within the frame. You click "Add to Cart" on a product card. The button changes to "Added!", AND the little Cart Icon in the navbar updates its number. πŸ‘‰ Use a Stream. You are touching two completely different areas of the DOM (The product card and the Navbar). You scroll to the bottom, and more items appear. πŸ‘‰ Use a Frame. Specifically, a lazy-loading frame with loading="lazy" pointing to the next page of pagination. Think of Frames as Scoping. You are narrowing the scope of interaction to a specific HTML element (<div>). Think of Streams as Instructions. You are sending a command list (append, remove, update) to the browser to execute wherever it pleases. Start with Frames. If you hit a wall where you need to affect something outside the box, graduate to Streams. Which one do you find yourself using more often? Let me know below! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: <turbo-frame id="message_<%= message.id %>"> <p><%= message.content %></p> <%= link_to "Edit", edit_message_path(message) %> </turbo-frame> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <turbo-frame id="message_<%= message.id %>"> <p><%= message.content %></p> <%= link_to "Edit", edit_message_path(message) %> </turbo-frame> CODE_BLOCK: <turbo-frame id="message_<%= message.id %>"> <p><%= message.content %></p> <%= link_to "Edit", edit_message_path(message) %> </turbo-frame> COMMAND_BLOCK: def edit @message = Message.find(params[:id]) # Rails renders 'edit.html.erb' automatically. # Because the request came from inside a Frame, # Turbo extracts the matching Frame from the response and swaps it. end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: def edit @message = Message.find(params[:id]) # Rails renders 'edit.html.erb' automatically. # Because the request came from inside a Frame, # Turbo extracts the matching Frame from the response and swaps it. end COMMAND_BLOCK: def edit @message = Message.find(params[:id]) # Rails renders 'edit.html.erb' automatically. # Because the request came from inside a Frame, # Turbo extracts the matching Frame from the response and swaps it. end CODE_BLOCK: def create @message = Message.create(msg_params) respond_to do |format| format.turbo_stream format.html { redirect_to messages_path } end end Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: def create @message = Message.create(msg_params) respond_to do |format| format.turbo_stream format.html { redirect_to messages_path } end end CODE_BLOCK: def create @message = Message.create(msg_params) respond_to do |format| format.turbo_stream format.html { redirect_to messages_path } end end CODE_BLOCK: <!-- 1. Append the new message to the list --> <%= turbo_stream.append "messages_list", partial: "messages/message", locals: { message: @message } %> <!-- 2. Reset the form so they can type again --> <%= turbo_stream.replace "new_message_form", partial: "messages/form", locals: { message: Message.new } %> <!-- 3. Update the counter in the header --> <%= turbo_stream.update "msg_counter", plain: Message.count %> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <!-- 1. Append the new message to the list --> <%= turbo_stream.append "messages_list", partial: "messages/message", locals: { message: @message } %> <!-- 2. Reset the form so they can type again --> <%= turbo_stream.replace "new_message_form", partial: "messages/form", locals: { message: Message.new } %> <!-- 3. Update the counter in the header --> <%= turbo_stream.update "msg_counter", plain: Message.count %> CODE_BLOCK: <!-- 1. Append the new message to the list --> <%= turbo_stream.append "messages_list", partial: "messages/message", locals: { message: @message } %> <!-- 2. Reset the form so they can type again --> <%= turbo_stream.replace "new_message_form", partial: "messages/form", locals: { message: Message.new } %> <!-- 3. Update the counter in the header --> <%= turbo_stream.update "msg_counter", plain: Message.count %> - Turbo Frames allow you to decompose a page into independent contexts. - Turbo Streams allow you to deliver page changes over WebSocket (or HTTP) to multiple places at once. - If you change the furniture in the Kitchen Frame, the Bedroom Frame doesn't care. - If you click a link inside the Kitchen, the response replaces the Kitchen only. - βœ… Lazy Loading (e.g., loading a heavy chart after the page loads). - βœ… Tabbed content switching. - βœ… Inline editing / Simple CRUD. - One action (the user clicks "Send") triggers multiple changes across the stage. - The generic form resets + The new message appears at the bottom + The "Total Messages" counter updates in the header. - βœ… Appending/Prepending items to a list. - βœ… Updating multiple distinct parts of the page at once. - βœ… Real-time broadcasts (ActionCable) where the server pushes updates to other users. - βœ… Removing an element (fade out).