Hotwire Discussion

Finally Understanding <turbo-stream>

I was a bit confused by the <turbo-stream> tag, so much I ended up reading the source code till I figured them out. Here’s what I found.

<turbo-stream? More like <turbo-mutation

The <turbo-stream tag is a web component / custom element that when added to the DOM it will have some jQuery type side effect on the DOM. That’s it!

(Registered as web components here: turbo/src/elements/index.ts)

For example if there’s a div:

<div id="asdf">Before Side Effect</div>

and you add this to the DOM (you can use right-click and inspect element to do this)

<turbo-stream action="replace" target="asdf">
    <div id="asdf">LOOK, I HAVE BEEN REPLACED</div>

Then it will result in the DOM looking like this (replacing its contents with the template:)

<div id="asdf">

Does <turbo-stream open HTTP/Websocket connections? Nope.

At first, I thought that adding <turbo-stream to the DOM would open some web socket connection or something. Not so, they don’t do anything but mutate the DOM when they mount. This is done in Turbo by having the browser call connectedCallback which is automatically called whenever a custom element is added to the dom or mutated in any way.

The only “magic” left to understand is how turbo listens to fetch requests and form submissions using it’s StreamObserver. This class will listen to the fetch requests that meet the following criteria:

  • match on the content type of: text/html; turbo-stream (it’s ok if there’s other stuff there in there about UTF8 and such, it just checks that it’s a substring).
  • <turbo-stream> elements are at the root of the request (no wrapping in <body> tags or anything else)
  • <turbo-stream> elements have a <template tag as it’s direct child

When it finds a fetch that matches these criteria then it’ll just add those tags to the DOM and they have the before mentioned side effect(s).

But what about Websockets!?!

Admittedly I haven’t got websockets working yet in phoenix but it seems that websockets are actually just normal fetch requests to the browser, they just get upgraded to a socket. So by listening to fetch requests turbo is also listening to websockets.

All the connecting to a websocket and such has to be handled by your backend framework of choice. This is sadly a bit tricky to do in Phoenix because it doesn’t seem to let you set the headers of a phoenix channel, but with the understanding from above it’s easy to see that it’s not hard to hack around by just listening to a channel and adding the resulting HTML to the DOM.


hopefully that was helpful! I now think of <turbo-stream as a simple way for the server to execute jQuery like commands to the DOM. It’s surprisingly simple and yet works very well. Also I would say don’t be afraid to read Trubo’s code, it’s pretty cleanly written and github was letting me look up where classes were used like this:


I think this is something that I wish the Turbo docs did a better job of pointing out. I believe it’s mentioned that Turbo-streams can be activated by normal HTTP requests, but it seems that regardless there’s still been a lot of confusion about whether streams are websocket-dependent.

Yeah, I wish that too. In their defense when you’re using turob-rails a lot of the details of how this works is hidden from you and it does seem like they do those kinds of things (from what I’ve observed).

I haven’t made much of an effort to look at how the base Turbo implementation works. Just based on the turbo-rails source I’ve looked at, I’d guess Turbo listens to all submit events and then attempts to fetch a turbo-stream response from the server, correct?

Maybe we could work on a PR to the docs site to make the actual mechanism for responding to normal HTTP requests more clear.

Well, turbo sends up an accept header which the server can switch its behavior on. Yeah, it would be cool to make the docs more clear perhaps. Upon more careful reading, it obviously mentions it.

So this works for any fetch request? If so that’s pretty cool and I wasn’t aware of that before - I had assumed it was only listening to form submissions and links. I have a drag and drop interface that I’ve been building and upon dropping the element it updates some data using fetch so to have the new element be rendered and returned by the server.

Yeah, not sure about this point. Lately, I’ve run into problems trying to get it to work via JS. I resorted to doing a fetch request then adding the results to the DOM (Although this can be a bit awkward if your server returns an error, be mindful of that). I’m thinking that Turbo actually only listened to stuff done by the browser. Tell me, anyone, if I’m wrong.