Technical Creativity Technical Creativity

HowTo Mount Stimulus and React in the same Rails 7 App

One of our long standing founders codebase recently needed some love. For reference, this codebase started as a rails 5 app circa 2016 and has seen upgrades to Rails 6 and 7.

This post will document how we’ve approached the need to mount react components in the same app that we mount the rails 7 stimulus.

Process

Step 1. Give this hairy beast of a todo to Startuplandia Team Dev Thiago Durante (A Founder’s Favorite)
Step 2. Notice step 1

Checkout this awesome PR shot for how Thiago broke the work into a long list of subtasks

You might notice some pretty standard stuff in Thiago’s Todo list.

What you might not be clear about are these 4.

“Individually test each new framework default”
“Migrate Turbolinks to Turbo”

The first two might seem normal enough but when you smush them together with switching out webpacker to Vite and co-mounting react inside stimulus, you end up in a funny new place.

“Migrate webpacker to Vite”
“Replace react_rails with Stimulus controller for mounting the react components”

Let’s examine some key pieces of code that we used…

Some of this you would recognize;

# app/views/layouts/application.html.erb

<%= vite_client_tag %>
<%= vite_react_refresh_tag %>
<%= vite_javascript_tag 'application' %>
// app/javascript/entrypoints/application.js
import '@hotwired/turbo-rails'
import '~/controllers/index.js' // Stimulus JS

but then things get different in;

// app/javascript/controllers/index.js
import { application } from './application'
import { registerControllers } from 'stimulus-vite-helpers'

// Controller files must be named *_controller.js.
const controllers  = import.meta.globEager('./**/*_controller.{js,jsx}')

specifically, since we continue to use a js build tool, vite powered by esbuild, we can’t eager load controllers “the turbo way” we had to figure out how to load them “the vite way”

const controllers  = import.meta.globEager('./**/*_controller.{js,jsx}')

Then, we’d be moving down the complexity line by looking for “react-factory-controller” (how’d you know the name of that component? (the rails wizards told me…) ) which contains all kinds of code including

// app/javascript/controllers/react_factory_controller.jsx
static values = {
  component: String,
  props: String,
  afterMountCallback: String
}

connect() {
  // [...]
  this.reactComponent = lazy(() => this.reactComponentPromise)
  this.createReactRootComponent()
  this.renderReactComponent()
  // [...]
}

createReactRootComponent() {
  this.reactRoot = createRoot(this.element)
}

// THIS IS WHERE DA MAGIC HAPPENS
renderReactComponent() {
  const $ReactComponent = this.reactComponent
  const parsedProps = JSON.parse(this.propsValue)

  const callbackProps =
    !!this.afterMountCallbackValue
      ? { callback: () => window[this.afterMountCallbackValue]?.() }
      : {}

  this.reactRoot.render(<Suspense><$ReactComponent {...parsedProps} {...callbackProps} /></Suspense>)
}

Why, Why, Why?

We need to know what compononent we want to instantiate, we need to know their props and then execute code after the react component mounts

And then… In a normal erb container we can call…

  <div
    data-controller="react-factory"
    data-react-factory-component-value="orders_container"
    data-react-factory-props-value="<%= react_component_props.to_json %>"
  ></div>

And the above will enable legacy react componentry to be run in a Rails 7, stimulus/hotwire paradigm.

React Factory Controller begins a rather serious mess of related to the fact that we have transitioned from server side rendered react-rails to primarily client side renderd stimulus/turbo patterns. So effectively, we need stimulus/turbo to “mount” and then we tell them to go look through the bowels of js for a bunch of more complex react component to mount inside already mounted context. (A mount inside a mount?)

and then

Oh By The Way…

Some code we needed to write that damn, we are not proud of…

connect() {
  const componentSplit = this.componentValue.split('/')

  switch (componentSplit.length) {
    case 1:
      this.reactComponentPromise =
        import(`@components/${componentSplit[0]}.jsx`)
      break
    case 2:
      this.reactComponentPromise =
        import(`@components/${componentSplit[0]}/${componentSplit[1]}.jsx`)
      break
    case 3:
      this.reactComponentPromise =
        import(`@components/${componentSplit[0]}/${componentSplit[1]}/${componentSplit[2]}.jsx`)
      break
    case 4:
      this.reactComponentPromise =
        import(`@components/${componentSplit[0]}/${componentSplit[1]}/${componentSplit[2]}/${componentSplit[3]}.jsx`)
      break
  }
}

We’ll give you a free startuplandia ultimate grade frisbee if you can tell us why we had to write this.

If you need help upgrading a rails 6 to 7 project or debugging running client rendered react components alongside stimiulus, reach out [email protected]

Have a great day,

The Devs of StartupLandia
[email protected]

Talk

A Dream Team

We have traveled many miles.

We will help you build, scale, grow.