Turbo Streams is pretty awesome.
It’s a great tool to make surgical updates to your pages over websocket/SSE. You can easily build applications that require live updates with the kind of minimal effort that would make the modern Javascript developer blush. And even more impressive, it’s made possible by reusing your existing Rails view partials, giving it essentially 8 new super powers.
It’s a fine tool to have. I must have it.
Earlier this year, we announced Superglue: The Rails Way of building React and Rails applications. In the same spirit, today we’re announcing Superglue 2.0 Alpha and our new tooling: Super Turbo Streams, a port of Turbo Streams made for Superglue.
Why?
Javascript fatigue. Turbo Stream’s promise of writing no additional javascript for streaming surgical updates is an amazing proposition, and if we can bring that to React, that would be – thoughtful. React doesn’t have a strong streaming story with Rails. There’s nothing out-of-the-box like Turbo, and rolling your own is tedious, error prone, and without well-shaped state, you’re often left figuring out how to put everything together.
How does it work?
Like Turbo Streams, the key to Super Turbo Streams “is the ability to reuse
your existing server-side templates to perform live, partial page changes.” The
difference is the delivery mechanism, instead of HTML, its JSON, i.e., we’ll
be using _post.json.props
instead of _post.html.erb
. Here’s an example
Lets assume you have
// app/views/posts/index.jsx
import React from 'react'
import {
useContent
} from '@thoughtbot/superglue'
export default function PostIndex() {
const {
favPost,
numOfPosts
} = useContent()
return (
<div>
<h1>{`There are ${numOfPosts}`}</h1>
<Post {...favPost} />
</div>
)
}
and the equivalent index.json.props
json.favPost do
json.title @post.title
json.body @post.body
end
json.numOfPosts Post.count
Here’s what we do:
First, lets generate the channel props to stream from using stream_from_props
,
the Superglue equivalent of turbo_stream_from
.
+ json.streamFromPosts stream_from_props('my_posts')
+ json.favPost(partial: ["post", fragment: "post-#{@post.id}"]) do
- json.favPost do
- json.title @post.title
- json.body @post.body
end
json.numOfPosts Post.count
We’ll also need to extract a partial and give it a fragment name, e.g. post-1
.
A fragment is a rendered partial with a frontend identity. You can think of it
as a DOM id, but for JSON.
# app/views/posts/_post.json.props
json.title @post.title
json.body @post.body
Then import useStreamSource
and pass the channel props.
// app/views/posts/index.jsx
import React from 'react'
import {
+ useStreamSource,
useContent
} from '@thoughtbot/superglue'
export default function PostIndex() {
const {
+ streamFromPosts,
favPost,
numOfPosts
} = useContent()
+ useStreamSource(streamFromPosts)
return (
<div>
<h1>{`There are ${numOfPosts}`}</h1>
<Post {...favPost} />
</div>
)
}
And finally update it using broadcast_save_later_to
(the combined equivalent of
Turbo’s broadcast_replace_later_to
and broadcast_replace_later_to
) from your controller, job, etc.
# Target using the fragment id from earlier
@post.broadcast_save_later_to('my_posts', target: "post-#{@post.id}")
Stream Responses
Streaming responses also work well. Here’s an example controller action:
def update
.....
if @post.save
respond_to do |format|
format.html { redirect_to posts_path, notice: "Successfully updated" }
format.json {
flash.now[:notice] = "Successfully submitted"
render layout: "stream" # The stream layout is generated for you
}
end
else
redirect_to posts_path, alert: "Could not update"
end
end
and the view update.json.props
broadcast_save_props(model: @post, target: "post-#{@post.id}")
Breaking changes
We caution that while in alpha, Superglue’s API may change. In its current state, Super Turbo Streams only supports 4 actions (prepend, append, save, refresh), we’re working to expand that number.
2.0 is also a backward breaking change from 1.0 as we’re redefining the idea of a fragment. A migration plan will be included in a major release.
We’re not done.
We’re working on more ways to make React and Rails play nice without sacrificing Rails conventions. Stay tuned!