Superglue 2.0 Alpha: React ♥️ Rails Turbo Streams!

logo

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!

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.