Rails + Stimulus + React

Dan Bridges
Parallel Thinking
Published in
4 min readMar 2, 2020

In this post I am going to demonstrate how to use prebuilt React components in an existing Rails and Stimulus project. Many of the traditional jQuery based UI control libraries, like select2 and various date pickers, have gone stagnant and are no longer actively developed. There is no question that the most actively developed UI libraries are for React, Vue, and other modern front end web frameworks. While React and friends have a place, a huge number of web apps are not served by going full SPA, with their added complexity. By utilizing React for common UI controls, and retaining the vast majority of our other views as traditional Rails partials, we can leverage the React community while retaining the simplicity of server rendered HTML for the bulk of our app.

Here we will be proxying a native multi-select with react-select, which will be embedded in a Stimulus controller that handles change events. Our app will be a simple list of filterable foods, which fall into 3 different categories: fruit, vegetable, and dairy. Even though we only have a few items we’ll be doing our filtering on the server to replicate a typical application.

The full example can be found here: https://github.com/dbridges/stimulus-and-react

The completed app using react-select

Stimulus

We start with a fairly standard Stimulus setup. Our Rails home controller renders the multi-select and the list of filtered foods for the initial value of “fruits”. These elements are wrapped in a food Stimulus controller which looks for a change event on the multi-select, then fetches the updated lists of foods based on the current selection, and finally replaces the food list HTML with the response HTML.

Our initial Stimulus implementation with a native select

React

We’ll be using react-select as a proxy for our native select. This allows us to continue using our original ruby helpers for generating the select and its options — we’ll translate the generated HTML into props for our react-select. We’ll also add a little Rails helper method to wrap our native select in a new Stimulus controller to manage its changes.

Lets add React to our project:

bundle exec rails webpacker:install:react

We’ll also add the `react-select` library:

yarn add react-select

On the Rails side we first introduce a react_select view helper that wraps its contents in an appropriately named Stimulus controller directive:

def react_select(**opts)
tag.div(**opts.deep_merge(
data: { controller: "react-select" })) do
yield
end
end

When rendering the select tag, we wrap it in our new helper:

<%= react_select do %>
<%= select_tag "kinds",
options_for_select(Food.kinds.keys, "fruit"),
multiple: true,
data: { action: "change->food#filter" } %>
<% end %>

We need to create a new Stimulus controller to manage the react-select. This is a bit more complicated so we’ll go through it piece by piece.

Our initialize method first checks to see if this controller has already been initialized. This is required to handle Turbolinks’ behavior of loading a cached version of the page when navigating back. When this cached version is loaded Stimulus tries to re-initialize all of the controllers again.

export default class extends Controller {
initialize() {
if (!this.data.get("initialized")) {
this.initReactSelect();
}
}
...
}

If this is a fresh initialization initReactSelect is called. First we grab our embedded select and hide it.

initReactSelect() {
const select = this.element.querySelector("select");
select.style.display = "none"; ...
}

We then assemble the props for react-select by reading attributes from the embedded native select:

initReactSelect() {
...
const options = [...select.options]; const defaultValue = [...select.selectedOptions] const isMulti = select.getAttribute("multiple") != null; ...
}

The onChange handler is the most complicated:

initReactSelect() {
...
const onChange = value => {
if (!Array.isArray(value)) {
select.value = value.value;
} else {
const selected = value.map(opt => opt.value);
for (const opt of select.options) {
if (selected.includes(opt.value)) {
opt.setAttribute("selected", "selected");
} else {
opt.removeAttribute("selected");
}
}
}
select.dispatchEvent(new Event("change"));
};
...
}

First we test if the value coming from react-select is an array or not. If it is a single select the value is just passed directly back and we simply assign it to our select and dispatch a change event. If there are multiple values we need to iterate through the options of our embedded native select and update their selected attribute before dispatching a change event.

Next we create a div element and mount a react-select to it:

initReactSelect() {
...
const reactSelect = document.createElement("div");
select.parentNode.insertBefore(reactSelect, select);
ReactDOM.render(
<Select
options={options}
defaultValue={defaultValue}
onChange={onChange}
isMulti={isMulti}
/>,
reactSelect
);
...
}

Finally we assign a data attribute to our controller so we don’t accidentally recreate the react-select on subsequent initializations.

initReactSelect() {
...
this.data.set("initialized", true);
}

And that is it! We didn’t even have to modify our original Stimulus `food` controller! For any other selects in our app we simply need to wrap them with our react_select helper to transform them into nice modern multi-selects.

I’ve found utilizing React as a replacement for common UI controls to be very powerful. It lets you avoid complex state management on the front end: no redux, no react router, no context, just some simple props. By isolating our React usage to common controls we can keep our application specific views and partials in Rails, which greatly simplifies the structure of the app and also allows us to leverage all of the nice Rails view helpers. This makes apps faster and easier to build, but still lets us bring in a little bit of React when we need it.

--

--

Dan Bridges
Parallel Thinking

Software developer at Beezwax Datatools and former researcher in Physics & Neuroscience.