Introducing Superglue: React ❤️ Rails

At thoughtbot, we rely on Rails to build applications at high velocities. When faced with adding modern client-side interactivity, we may have many forms of progressive enhancement to turn to. In particular, gaining popularity within the team are the trio of companion libraries: Hotwire, Turbo, Stimulus. These frameworks make it easy to add sprinkles of interactivity to HTML and have been a much easier option to reach for than React, Redux, and friends.

But React is so tempting!

Components make organizing testable units of interactivity a breeze. Being declarative, it’s a lot easier and sensible to manipulate state as a simple data structure than to carefully manipulate the DOM. And while Hotwire and friends excel at quickly building light to medium complexity, React excels at that and beyond.

Why can’t we have it both ways?

Introducing Superglue. A framework that makes building Rails, React, and Redux applications as productive as Rails, Hotwire, Turbo, and Stimulus applications. It brings a developer experience that is as much classic Rails as it is normal React and Redux.

It’s Rails

Superglue leans on Rails’ ability to respond to different mime types on the same route and divides the usual foobar.html.erb into three familiar templates.

  • foobar.json.props A presenter written in a jbuilder-like template that builds your page props.
  • foobar.js Your page component that receives the props from above.
  • foobar.html.erb Injects your page props into Redux when the browser loads it.

Shape your props to roughly how your components are presented. For example:

json.header do
  json.username @user.username
  json.linkToProfile url_for(@user)
end

json.rightDrawer do
  json.cart(partial: 'cart') do
  end
  json.dailySpecials(partial: 'specials') do
  end
end

json.body do
  json.productFilter do
    form_props(url: "/", method: "GET") do |f|
      f.select(:category, ["lifestyle", "programming", "spiritual"])
      f.submit
    end
  end

  json.products do
    json.array! @products do |product|
      json.title product.title
      json.urlToProduct url_for(product)
    end
  end
end

json.footer do
  json.copyrightYear "2023"
end

Familiar Rails conveniences include form props, a fork of form_with made for React; the flash is integrated as a Redux slice; and Unobtrusive Javascript helpers.

It’s React

But there are no APIs! The above is injected as a script tag in the DOM so everything loads in the initial request. Its added to your Redux state and passed to foobar.js as props, for example:

import React from 'react'
import { useSelector } from 'react-redux'
import { Drawer, Header, Footer, ProductList, ProductFilter } from './components'

export default function FooBar({
  header,
  products = [],
  productFilter,
  rightDrawer,
  footer
}) {
  const flash = useSelector((state) => state.flash)

  return (
    <>
      <p id="notice">{flash && flash.notice}</p>
      <Header {...header}>
        <Drawer {...rightDrawer} />
      </Header>

      <ProductList {...products}>
        <ProductFilter {...productFilter} />
      </ProductList>

      <Footer {...footer} />
    </>
  )
}

At heart, Superglue is a fork of Turbolinks 3, but instead of sending your foobar.html.erb over the wire and swapping the <body>, it sends foobar.json.props over the wire to your React and Redux app and swaps the page component.

This behavior is opt-in. Superglue provides UJS helpers that you can use with your React components to SPA transition to the next page.

<a href=”/next_page data-sg-visit> Next Page </a>

It’s more!

Being able to easily use React in place of ERB isn’t enough. Superglue’s secret sauce is that your foobar.json.props is diggable; making any part of your page dynamic by using a query string. It’s a simpler approach to Turbo Frames and Turbo Stream.

Need to reload a part of the page? Just add a query parameter and combine with the UJS helper attribute data-sg-remote:

<Header {...header}>
  <Drawer {...rightDrawer} />

  <a data-sg-remote href='/some_current_page?props_at=data.rightDrawer.dailySpecials'>
    Reload Daily Specials
  </a>
</Header>

The above will traverse foobar.json.props, grab dailySpecials while skipping other nodes, and immutably graft it to your Redux store.

This works well for modals, chat, streaming, and more! All deserving a blog post of its own.

Should I use Hotwire or Superglue?

With Superglue you have a new, yet familiar option for building apps rapidly. Superglue does require maintaining three separate templates instead of one, but it makes building medium to complex functionality comparatively easier than Hotwire and friends. A non-trivial application built in both Stimulus, and Superglue is available if you’d like to compare and contrast the two approaches.

For some, Hotwire and friends is enough, but for those who believe that components and modern interactity is React and Redux’s strength, I encourage you give Superglue a try.