The current state of the Rails Framework is nearly unanimous among developers, as it encompasses everything a web application needs from start to finish. It covers everything from infrastructure setup to the quick and easy construction of interactive views—without relying on React or any other frontend libraries or frameworks. With just a few simple steps, you can deploy your application on a cloud platform like Heroku, making it accessible to multiple users.
In this post, we will explore the frontend capabilities of Rails, where Stimulus excels, and demonstrate how it can work alongside the React.js library.
So what is Stimulus anyway?
Stimulus is a JavaScript framework with modest ambitions. Unlike other front-end frameworks, Stimulus is designed to enhance static or server-rendered HTML — the “HTML you own” — by connecting JavaScript objects to page elements through simple annotations.
These JavaScript objects are called controllers, and Stimulus continually monitors the page for data-controller
attributes in the HTML. For each attribute found, Stimulus parses its value to find a matching controller class, creates a new instance of that class, and attaches it to the element.
You can think of it like this: just as the class
attribute is a bridge that connects HTML to CSS, Stimulus’s data-controller
attribute is a bridge that connects HTML to JavaScript.
Given this brief library description, you might think: "Oh, Stimulus is super easy to work with; it’s a React, Vue, or Angular killer." In some cases, using Stimulus is sufficient and even highly recommended. However, libraries like React, Vue, and Angular (the latter to a lesser extent 😅) are still quite popular. They come with extensive documentation, numerous forums for finding solutions, and large communities. Many developers who are more familiar with these frameworks can find it challenging to adapt to the world of Stimulus, especially when working on MVC Rails projects (I include myself in this group).
It is also worth mentioning that there are Rails gems that, in theory, facilitate the integration of React with Rails, for example. But my experience with them has not been very positive. Some of the gems I have tried include:
So, if you identify with this group of developers who have difficulty adapting to Stimulus, let’s learn how to integrate React.js with a Rails + Stimulus app in a specific use case, which can also be adapted for other scenarios.
Let’s integrate!
The following diagram shows how we will structure the things to work together.
Let’s assume you already have a Rails MVC application pattern set up on the most current version of the Rails framework.
Firstly, let’s update our Gemfile
.
- gem "importmap-rails"
+ gem "jsbundling-rails"
gem "turbo-rails"
gem "stimulus-rails"
Next step: create package.json
in the project root with the following script and dependencies.
{
"name": "app",
"private": true,
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets"
},
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.12",
"@rails/actiontext": "^8.0.100",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"esbuild": "^0.24.0"
}
}
Let’s update app/javascript/application.js
.
import "@hotwired/turbo-rails";
import "./controllers";
import "@rails/actiontext";
Next app/javascript/controllers/index.js
with the following content:
// This file is auto-generated by ./bin/rails stimulus:manifest:update
// Run that command whenever you add a new controller or create them with
// ./bin/rails generate stimulus controllerName
import { application } from "./application"
import AutocompleteController from "./autocomplete_controller"
application.register("autocomplete", AutocompleteController)
But, If you’ve added stimulus controllers, you may need to run:
./bin/rails stimulus:manifest:update
Now, let’s update the layout app/views/layouts/application.html.erb
with those tags:
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
Finally, update or create the Procfile.dev
to add esbuild
for JS which will be responsible for watching the JavaScript changes.
web: bin/rails server
+ js: yarn build --watch
Now, let’s write our view .erb
file, controller, and component.
<%= form_for @product do |f| %>
<%= f.label :name %>
<div data-controller="autocomplete">
<div data-autocomplete-target="root"></div>
<%= f.hidden_field :name, as: :hidden, data: { target: 'autocomplete.selected-option' } %>
</div>
<br>
<%= f.submit class: 'btn' %>
<% end %>
This simple form will be responsible for saving a product record using the Product
ActiveRecord
model. The Stimulus controller will render the React Autocomplete component in the root
data target, enabling users to search for a product by name and select it to save it in our database.
The f.hidden_field
will receive the value selected from the autocomplete.
Now let’s understand how the Stimulus autocomplete_controller.js
will manage (connect, render, and destroy) the react autocomplete component.
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
//Load the React code when we initialize
initialize() {
this.componentPromise = import("./components/autocomplete");
}
async connect() {
this.component = await this.componentPromise;
const root = this.targets.find("root");
const selectedOption = this.targets.find("selected-option");
const props = {
value: selectedOption.value,
onSelected: this.onSelected.bind(this),
};
this.component.render(root, props);
}
onSelected(value) {
this.targets.find("selected-option").value = value;
}
disconnect() {
const root = this.targets.find("root");
this.component.destroy(root);
}
}
Let’s deep dive into the key methods from the Stimulus lifecycle:
initialize
: This method is responsible for just importing the React component as a promise (pending) that will not be loaded at the same time the page is loaded.connect
: This method will be responsible for resolving our component import promise and rendering it informing the required props.disconnect
: Finally, this method will be responsible for destroying, and removing from the DOM, our component when the controller is disconnected.
Now our React Autocomplete Component! It is defined like this:
import React from "react";
import { createRoot } from "react-dom/client";
import { AutoComplete as AntdAutoComplete } from "antd";
const products = [
{ value: "Nike Shoes" },
{ value: "Adidas Shoes" },
{ value: "Apple Watch" },
];
const Autocomplete = ({ value: valueProp, onSelected }) => {
const [value, setValue] = React.useState(valueProp);
const [options, setOptions] = React.useState(products);
const onSelect = (data) => {
onSelected(data);
};
const onChange = (data) => {
setValue(data);
};
const getPanelValue = (searchText) => {
return options
.filter((option) =>
option.value.toLowerCase().includes(searchText?.toLowerCase())
)
.map((option) => ({ value: option.value }));
};
return (
<AntdAutoComplete
value={value}
options={options}
style={{
width: 200,
}}
onSelect={onSelect}
onSearch={(text) => setOptions(getPanelValue(text))}
onChange={onChange}
placeholder="control mode"
/>
);
};
let root = null;
function render(node, props) {
root = createRoot(node);
root.render(<Autocomplete {...props} />);
}
function destroy() {
root.unmount();
}
export { destroy, render };
This component is a simple wrapper of the Autocomplete input from the Ant React UI framework. As you can see, the implementation of the component is transparent without using anything from Stimulus 🤓. But it’s necessary to explain the two last methods: render
and destroy
.
render
: Responsible for creating the component in theroot
data target declared/defined on our view;destroy
: Only responsible for unmounting the component, in other words, removing a component from the DOM.
In the end that is the final result:
One of the advantages of this approach is to only load the React.js where necessary, without taking control of the whole Rails frontend area. For example, in the index page (image below), where is only rendered the list of the products, the react lib is not loaded.
Whereas on the New product page, the React.js is loaded due to the rendering of the Autocomplete component by the Autocomplete Stimulus controller connect
method, as mentioned before.
Therefore, integrating React.js with Rails + Stimulus can be a powerful approach for building modern web applications. By leveraging Stimulus for lightweight frontend interactivity and React for more complex components, you can create a seamless developer experience while keeping your application performant and maintainable. The example presented demonstrates how these tools complement each other, making it easier to gradually enhance your Rails application without fully adopting a single-page application architecture.
Well, that’s all folks! If you arrived in the end, you are the greatest! I will see you in the next one.
Source code presented in this blog post:
https://github.com/joaoGabriel55/rails8-stimulus-react
References
We want to work with you. Check out our "What We Do" section!