30 · the limit of a frame

Post from inside a frame.

The list and the compose box now sit together inside <turbo-frame id="board">. The counter in the black bar is outside it.

Place your bet. After you post, does the counter go up?

→ post a message
30 · Turbo Streams

Let the server send instructions instead of a page.

Switch the create action to answer with a stream. The frame around the list is gone too: a stream targets any element with an id, framed or not. Post again. The response is no longer HTML for a page. It's a list of <turbo-stream> elements, each with an action, a target id, and a <template> of HTML to use.

Three things flash: the new message appended to the list, the counter updated in the nav, the form replaced with an empty one. Three places, one response, no navigation. The URL didn't even change.

→ switch it on, post
30 · how the browser asked

A stream is a response format, like .json.

When Turbo submits a non-GET form it adds text/vnd.turbo-stream.html to the Accept header. Rails sees that and picks the turbo_stream branch. Anything that can't take streams, like a plain browser or a native app, falls through to the redirect.

def create
  @message = Message.create!(message_params)

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

With no block, format.turbo_stream renders create.turbo_stream.erb, which is just a template of stream tags. Look at the response on the right. That's the file, rendered. For a GET link or form that should get a stream back, add data-turbo-stream.

30 · the partial you already have

One template, first load and every update after.

<%# create.turbo_stream.erb %>
<%= turbo_stream.append "messages", @message %>
<%= turbo_stream.update "message_count", Message.count %>
<%= turbo_stream.replace "composer", partial: "messages/form" %>

Passing @message renders _message.html.erb, the same partial the index page uses to draw the list. There is no second copy of the message markup living in JavaScript. That is the whole point of sending HTML instead of JSON: the server already knows how to draw a message, so let it.

Targets are plain ids. dom_id(message) gives you message_7; that's why every message div carries one. A stream can target any element with an id. It does not need to be a frame.

30 · the rule

A stream can't run JavaScript. On purpose.

Eight verbs and nothing else: append, prepend, replace, update, remove, before, after, refresh. If a new element needs behaviour, the HTML carries a data-controller and Stimulus wires it up when it lands. That's the next rung.

Three side paths from here: