Search code examples
ruby-on-railswebsockethotwire-railshttp-streamingturbo-rails

How to use Rails and Hotwire Turbo Streaming to Stream a Long Page?


I looked at https://turbo.hotwired.dev/handbook/streams and https://www.hotrails.dev/turbo-rails/turbo-streams , but did not see quite how to do this.

I have a long page that many messages. When the page loads, it should stream the messages. Suppose there are 100 messages and it takes 100ms to render each message. The user should see 1 message each 100ms so that they are rendered as they are streamed.

On the index page there would be a target id:

<div id="messages"></div>

It seems there would be a message partial:

<turbo-stream action="append" target="messages">
  <template>
    <turbo-frame id="message">
      <%= message.text %>
    </turbo-frame>
  </template>
</turbo-stream>

It seems that the controller might look like:

@messages = Message.last(100)

respond_to do |format|
  format.html do
  end

  format.turbo_stream do
    render turbo_stream: turbo_stream.append(:messages, partial: 'message',
      locals: { collection: @messages })
  end
end

Not sure if the whole page is streaming or only the messages would be streaming.

If the whole page is streaming, then I gather you must force Rails to render a turbo_stream with out a streaming request. But not sure how to target the _top frame.

If only the messages are streaming, then it seems we need a mechanism to start the streaming upon the rendering of the index page. Not sure if there is a JS Turbo command to do this...

Please do not tell me that this is the wrong thing to do. I know that it is a bad idea to stream many messages. This is simplified example of the use case. This simplified example makes it easier comprehend without adding additional context.

EDIT: Rails 7.1.2


Solution

  • Note that on its own turbo stream is not a streaming response, it's just a regular http/html. You could make your own Turbo channel and stream from that:

    # app/channels/messages_channel.rb
    
    class MessagesChannel < Turbo::StreamsChannel
      def subscribed
        super
    
        Message.last(100).each do |message|
          MessagesChannel.broadcast_append_to(
            verified_stream_name_from_params,
            target: "messages",
            partial: "messages/message",
            locals: {message:}
          )
        end
      end
    end
    
    # app/views/messages/_message.html.erb
    
    <%= tag.div id: dom_id(message) do %>
      <%= message.text %>
    <% end %>
    

    In your template:

    <%= turbo_stream_from "messages", channel: "MessagesChannel" %>
    
    <div id="messages"></div>