Table of Content

What if a Ruby on Rails application built using Stimulus.js needs to have more complex interactions, as well as a degree of compartmentalization and reusability for specific behaviors and displays? Surely, we can accomplish this in Stimulus. Of course, they can, but they may require an extra layer of work. Using JavaScript UI Framework would simplify this task. So you may think that the solution is to just create a separate front-end application with a UI Framework. This is not the optimal solution. So what other solutions are there ?

Various solutions allow using JavaScript frameworks sprinkled throughout a Ruby on Rails application, which do not require rewriting the entire client-side code. This article focuses on using the React framework and on the solution we reached which allowed us to keep the existing Stimulus controllers while introducing React.js components. It was the combination of turbo-mount and Vite Ruby.

What Is Turbo-mount and Vite Ruby

Turbo-mount

Turbo-mout is simple library that allows the addition of highly interactive components from React, Vue, Svelte, and other frameworks to a Hotwire application.

Using this mainly for the reduction of the boilerplate that is needed to create to integrate a react virtual DOM with a custom Stimulus controller. This approach allows us to start small and add components to existing views or when creating new views to build components that form the core of the view.

If the desire is to keep the application using only import maps this is a possibility. However, the JavaScript islimited to CommonJS which adds a layer of complexity.

Vite Ruby

This project includes a gem(vite_rails) that allows Vite to be set up and integrated into a new or existing Rails application. It provides a similar functionality to the old webpacker gem. Additionally, it gives access to front-end tooling and provides a simple configuration to keep the existing Stimulus code.

Furthermore, we can use a package manager to add new libraries, and we can keep the existing importmap.rb. And the code is no longer limited to CommonJS, meaning it enables the use of JSX in the components.

WARNING: Make sure there are no packages in config/importmap.rb that are also in package.json. These collisions can cause breaks or even strange behavior in the existing or new code.

Setup

Start by following the Vite Ruby’s ”Get Started”. This will generate some files. From these, we proceed to change the config/vite.json to indicate to Vite the source code folder and to create separation from the existing one that is for Stimulus.

{  "all": {    "sourceCodeDir": "app/frontend",    "watchAdditionalPaths": []  },  ...}

Now pointing to a folder that may not exist. So, let’s create it, and move the entrypoints folder from inside javascript folder to inside the new folder, leaving the folder structure like this:

app  ├── frontend:  |   ├── entrypoints:  │   |    └── application.js  |   ├── components:  |   ├── pages:  |   └── utils:  └── javascript:      ├── controllers:      └── application.js

Why move the entrypoints folder? We move is so the gem correctly load s Vite from that folder and not the original.

Now, let’s add turbo-mount to the project, using Yarn as our package manager.

yarn add turbo-mount

Then add the desired framework in this case React.

yarn add react react-dom

Additionally, Vite has a plugin(@vitejs/plugin-react) for React which enables Fast Refresh and some other features. Let’s also add it to make development easier.

yarn add -d @vitejs/plugin-react

To enable this plugin, we need to make some changes the vite.config.js but they are simple enough.

...import react from '@vitejs/plugin-react'...export default defineConfig({  plugins: [    ...,    react()  ],  ...});

Now with this, let’s add a new file, turbo-mount.js, inside the frontend folder

app  ├── frontend:  |   ├── entrypoints:  │   |    └── application.js  |   ├── components:  |   ├── pages:  |   ├── utils:  |   └── turbo-mount.js  └── javascript:      ├── controllers:      └── application.jsimport { TurboMount } from "turbo-mount";const turboMount = new TurboMount();

This file is where we register components so that turbo-mount library detects and allows their use in the Rails Views.

import React from "react"const TestComponent = ({ message }) => {  return (<div>      {message}</div>  )}export default TestComponent

This is an example component that we can be import into turbo-mount.js, then we register it.

...import { registerComponent } from "turbo-mount/react";// Componentsimport TestComponent from "../testComponent";registerComponent(turboMount, "TestComponent", TestComponent);

Now, let’s now import this file into the app/frontend/entrypoints/application.js

...import "../turbo-mount"...

With this, in a Rails view to use the registered component we just need to do this.

<%= turbo_mount "TestComponent", props: { message: "Hello World"}, class: "" %>

However, there may still be something missing. Therefore, we need to make sure that the new packages that will be used by the React components do not affect the import map. Let’s check the app/views/layouts to ensure the Vite config is after the import maps

...<%# Import Maps %><%= javascript_importmap_tags %><%# Vite %><%= vite_client_tag %><%= vite_react_refresh_tag %><%= vite_javascript_tag 'application' %>

Now with the setup complete, we can start development. There are a couple of changes we can make if we want to run only one command and have every log in the same place. To achieve this change, we must add the following vite: bin/vite dev in Procfile.dev

vite: bin/vite devweb: bin/rails server

If you want to keep it separate just run bundle vite dev in a separate terminal.

Deployment:

The Vite Ruby deployment section explains the configuration for deployment. The short explanation is that the vite build command is run in the assets:precompile. On the deployment pipeline, make sure to include the node image and run the package manager install so that the pipeline loads all the packages before running the assets:precompile.

Recommendation:

Fully migrate to Vite to not cause problems with loading orders in the browser by keeping the import maps. If you decided to keep the import maps and are running in to problems. You may need to change/add this flag in your production/development/test.rb files:

config.action_view.preload_links_header = false

Also if you do the full migration check out stimulus-vite-helpers because @hotwired/stimulus-loading is not published in npm and you may need it it if want to auto load your Stimulus controllers.

Possible Alternative:

react_on_rails: This is an alternative to sprinkle react components but you will be limited to React while using turbo-mount allows you to choose other Frameworks.

Conclusion

The solution presented here for changing from Stimulus to a more interactive Framework, in this case React.js, is a simple one.

The article demonstrates how straightforward it is to configure the solution and the changes needed to get React to work in an existing project by using Vite Ruby project and the turbo-mount library.

Creating a clear separation between the existing code and the new code makes it easier for fresh eyes to navigate the project. It also adds some future-proofing in case a stronger split between the front end and back end becomes necessary. Since Vite is a widely used front-end tool at the time of writing, we can handle this separation process more smoothly.