My introduction to Superglue: React ❤️ Rails

Dave Iverson

I just joined a new project building a Ruby on Rails app with some React bits. The React bits are implemented with Superglue. This was my first experience with the Superglue library, and these are my first impressions.

First, some context:

I used to build Rails apps often. Recently I’ve been writing JavaScript/React apps. I skimmed the Superglue docs, but I don’t know much about it. It seems to take some inspiration from Turbolinks. I have an aversion to Turbolinks from the old days.

The selling point of Superglue is that it makes it easier to get Rails data into a React app. I’ve often worked on projects where React calls a Rails API backend via AJAX requests. Maintaining this link is annoying: you must set up CORS, implement security, and define an API schema. Let’s find out if Superglue can simplify it.

thoughtbotter Johny Ho built Superglue and championed its use on this project.

So far with Superglue, I’ve:

  • edited an existing React component on a Superglue screen
  • made some AJAX requests to get dynamic data
  • created a new Superglue screen

The structure of a Superglue screen

The first thing I did on this project was to figure out what files are involved with Superglue. Our app has the concept of Superglue screens: entire screens built with React instead of Rails ERB templates.

Each Superglue screen has 3 files:

  • an index.html.erb template that sets up the initial state of the app (it’s pretty sparse)
<% initial_state = controller.render_to_string(@virtual_path ,formats: [:json], locals: local_assigns, layout: true) %>

<script type="text/javascript">
  window.SUPERGLUE_INITIAL_PAGE_STATE=<%= initial_state.html_safe %>;
</script>
  • an index.jsx file that defines the page’s React component
export default function DashboardIndex(props) {
  const {flash, alert} = props;
  return <div>
    <Flash flash={flash} />
    {alert && <Alert {...alert} />}
    Welcome to the dashboard!
  </div>
}
  • an index.json.props file that declares the props for the React component to receive
if current_user
  json.first_name current_user.profile.first_name
  json.alert Alerter.for_dashboard(current_user, self)
else
  json.first_name "Guest"
end

Editing a Superglue React component

Modifying an existing component was easy. It’s just writing JSX. Fortunately, this project is already configured with Webpacker. I didn’t have to fight the build system. To make UI changes, I wrote them to the JSX file, saved it, and reloaded the webpage!

Editing the props for an existing component was also easy. But I did have to learn how props work. The index.json.props file uses JBuilder, a Ruby DSL, for building JSON. Ruby sure does like DSLs. It’s not hard to work with, especially when I figured out how to make my editor use Ruby syntax highlighting.

I don’t know when logic and helper methods should go in the index.json.props file vs. in the controller. I declared a helper function and did some calculations in the JSON props file. Maybe that should have happened in the controller? Presumably, this props file works like a view and can access any instance variables in the controller’s scope.

The cool thing about this JSON props file is that I can preview it! Loading http://localhost:3000/dashboard.json returns the contents of the JSON. The React component will get all this data as props.

Here’s what the generated JSON looks like:

{
  "data":{
    "firstName":"Dave",
    "alert":{
      "type":"warning",
      "title":"Your bank account balance is negative.",
      "body":"How do I get back to healthy account balance?"
    }
  },
  "componentIdentifier":"dashboard/index",
  "defers":[],
  "fragments":[],
  "assets":[
    "/packs/js/application-da6da53e60e0483f9c77.js",
    "/assets/application-3e41d1709623783400ffae3905ec7116cc906199c9fa52475a2dce6cd9c30fab.css"
  ],
  "csrfToken":"kRpOJQ4UjuhN9hqXClMxz7Cs60Yp1PgG1KmjeCpTh68fEzwdeI6dyThL50Ko5WwKBz528qRvk4GrVXdMpvX50Q",
  "restoreStrategy":"fromCacheAndRevisitInBackground",
  "renderedAt":1678900171,
  "flash":{}
}

I don’t know what’s up with all that metadata at the end. What does Superglue do with it? Let’s ignore those JSON fields for now.

I also discovered that I can pass URL parameters to this dashboard endpoint. I can use that to preview how it works with different arguments.

Adding AJAX to a Superglue component

Next, I needed to try out the AJAX features of Superglue. My new React screen has a set of tabs. Each tab shows different data in a chart. Superglue can initialize a React component with the first tab’s data. I think I can make it dynamically load another tab’s data when you click that tab.

This bit was quick and straightforward, but I still don’t understand how it works. I found an example to copy from another file. In the props file, it’s easy to generate different data. We’ll receive a URL parameter (params[:tab_name]), and we can use it to return different props data:

if params[:tab_name] == "month"
  json.data monthly_chart_data
else
  json.data weekly_chart_data
end

We can also return some information about the tab so that React knows whether to highlight it and what URL it should use to request updated props.

json.month_tab do
  json.path send(index_path, {
    cashflow_interval: :month,
    props_at: props_at
  })
  json.is_active params[:tab_name] == "month"
end

I don’t understand what that props_at URL parameter is doing, why we need it, or even where the variable comes from.

Here’s how I built the tabs in React:

const { weekTab, monthTab } = props;
...
<div className="tab__container">
  <a
    href={weekTab.path}
    aria-label="Weekly"
    data-sg-remote={true}
    className={`tab button ${
      weekTab.isActive && 'active'
    }`}
  >
    Week
</a>
  <a
    href={monthTab.path}
    aria-label="Monthly"
    data-sg-remote={true}
    className={`tab button ${
      monthTab.isActive && 'active’
    }`}
  >
    Month
</a>
</div>

That data-sg-remote data attribute must tell Superglue to make an AJAX call and rerender this React component with the new props. And we can check the monthTab.isActive prop to figure out if that tab is currently active.

Building a new Superglue screen

It was not hard to make a new screen. A little confusing. First, I made the 3 files (onboarding/index.jsx, onboarding/index.html.erb, and onboarding/index.json.props). In the ERB file, I copied the boilerplate window.SUPERGLUE_INITIAL_PAGE_STATE from a previous screen.

Now that the ERB file was done, I could build a React component - the easy part for a React dev. But when I loaded the screen, it was blank, and there was an error in the console:

Uncaught Error: Superglue Nav component was looking for onboarding/index but could not find it in your mapping.

I guess new React files aren’t hooked up automatically. I found the mapping file in packs/application.js. This file seems to be where Superglue (and Redux) are initialized. I found the spot to add the new mapping:

const identifierToComponentMapping = {
  'dashboard/index': DashboardIndex,
    // NEW:
  'onboarding/index': OnboardingIndex,
}

But how does Superglue figure out which path to use? It’s not a URL path (/onboarding). Might it be a reference to the controller and action? 🤷 Whatever, it works now. My component appears on the screen.

I also tried writing some HTML in the onboarding/index.html.erb template file to see if it would appear alongside the React app. But it didn’t appear on the page. Frustrating. I found that our Superglue layout file (views/layouts/superglue.html.erb has a <%= yield %> inside the React div.

<!DOCTYPE html>
<html lang="en">
  <%= render "application_head" %>
  <body>
    <div class="application-container">
      <%= render "application_nav" %>
      <main>
        <div id="app" class="application-inner-container">
          <%= yield %>
        </div>
        <%= render "javascript" %>
      </main>
    </div>
  </body>
</html>

As a result, any HTML acts as no-js content - it gets replaced by the React app. That’s fine, but I might move the yield outside that div. Then I can build a hybrid screen: half React and half ERB. It would be especially nice if I could inject ERB template content INSIDE the React app. But I don’t think it’s possible.

Every controller and view can decide whether to render ERB or Superglue React. The controller chooses which layout to use: render layout: "superglue" or render layout: "application".

The final step is to hook up some props for the new screen. I edited the new onboarding/index.json.props file. (Why is this called .json.props instead of .props.json or .json.rb? I don’t know.)

I added a quick prop:

json.first_name "Guest"

and confirmed that my component received it. Just like magic!

General thoughts about Superglue

There’s a lot of magic going on that I don’t quite understand. Then again, this is Rails, and magic is par for the course.

Here are some of the things I still don’t get:

  • what’s Redux doing, and do I need to care?
  • what’s the props_at thing I see in the props files?
  • how does Superglue know which JS file to load?
  • can I render ERB code inside a React component?
  • how does data-sg-remote work?
  • why are the Jbuilder variable names snake_cased, but the JS props are camelCased?

I should read the docs.

I wish Superglue’s AJAX calls were more intuitive. I doubt I could build something complicated with that little data-sg-remote attribute.

I still haven’t learned how a POST request would work with Superglue. Can you submit a form and have Rails perform the validation and return JSON error responses?

I wish Superglue supported an “islands” architecture. I’d love to build screens with a few mini React components sprinkled inside an ERB template. But it seems to only support one React component per page.

What I appreciate the most about Superglue is that I don’t need to write an API and AJAX requests for React to talk to Rails. I love that I can just put a Rails variable in the JSON props file, and React will automatically receive it. This alone makes Superglue valuable to me.

So the big question is: would I use Superglue on a new project? Maybe. I’d use Superglue if:

  • I only plan to have a few React screens
  • I don’t need to use a React client-side router
  • The React stuff is read-only (no forms)
    • I might change my mind when I learn more about this
  • Installing Superglue in a new Rails app isn’t too hard

As I keep working with this framework and learning its patterns, I’m growing more attached to it. Superglue is still young, and I expect it will become more approachable with time. I’m excited that folks like Johny are working on ways to keep Rails relevant in a React-heavy world.

Next Up

I’ve shared this post with Superglue creator Johny and he’s writing a response. He has some corrections, clarifications, and a peek under the hood. Look for that blog post soon!