How to use React just on one page in a Rails app

hand putting a page of react onto a rails app

You’re working on a “typical” Rails app, which serves HTML with sprinkles of JavaScript (HAML or ERB views with Stimulus handling the JS). You’re feeling pretty satisfied with how things are set up and how easily you’re able to build nice features with a decent amount of user-interactivity for your app. One day, your boss (or client, or team-mate) comes to you with a feature request. You quickly realize that the feature requires a high degree of user-interactivity. Left clicks, right clicks, drags, zooms, state, and so on. You start building this out with views and Stimulus, but find yourself wishing you had a more expressive and less messy way to handle events, states and just even the HTML itself.

Having had experience with React, you wonder if it is possible to use React just where it is needed, for your new feature, and continue to use plain old ERB and JS (Stimulus) everywhere else. “It’d be nice if I could just write a React component with JSX and drop it in to my Rails app, without overhauling how my app works and losing the productivity benefits of plain old ERB and JS. Heck, if that worked, there might even be some third party React library that I could leverage to help me go faster with this”, you reason.

Is it possible to do this without setting up a whole separate React app externally and making your Rails app support API requests from the front-end? It sure is. React is just JavaScript after all, even if you have to go through a transpilation step or two. Let’s go through a small example to see how this might be done.

The approach

We’re still going to use ERB/HAML (I’ll use HAML in my examples) and Stimulus. First, in our view, we’re going to create a container where our component is going to live. Then, we’re going to create a Stimulus controller and wire up the container to it. The Stimulus controller is going to be in charge of rendering the React component in the view. Finally, we’ll take a look at how the React component can interact with the server and some pitfalls to watch out for.

Add a DOM container for your component

This part is straightforward. Figure out where you want your component to live in your page/view and define an HTML container for it. For example, if our “high interactivity” component is going to be a fancy graph, we’d set up something like:

.fancy-graph-container

Wire up the container to your Stimulus controller

.fancy-graph-container{ data: { controller: "fancyGraph" }}

This will connect your container to a Stimulus controller defined in the file fancy_graph_controller.js. There are some benefits that come with using Stimulus:

  1. We don’t have to write our own Javascript to figure out when the DOM content has loaded, and ensure that the React component is only rendered after. Stimulus does this for us in the background, and we need only bother with rendering the component.
  2. If we want to trigger the rendering of the component off of some user-generated event, like a click, Stimulus with its built-in abstractions, makes it easy to do so.
  3. If we want to pass in props to the React component, and some of these props are dynamic (maybe they depend on some state that only your application server knows about on render), Stimulus makes it easy to pass these in as well. Instance variables in your Rails controller, which can be accessed in your view, can be passed in to the Stimulus controller using the Stimulus Values API, and Stimlus can then pass these values in as props to your React component.

Render the React component

In your Stimulus controller, you’d do:

import { Controller } from "@hotwired/stimulus";
import React from "react";
import ReactDOM from "react-dom";

import FancyGraphComponent from "/path/to/your/fancy_graph_component";

export default class extends Controller {
  connect() {
    ReactDOM.render(<FancyGraphComponent />, this.element);
  }
}

This will, on page load, initialize your React component and render it in .graph-container (this.element is a Stimulus shorthand that gives you access to the element to which the controller is wired to). And that’s it, really! You now have a React component running inside your Rails app, and haven’t done that much work to achieve it. You can go further than this, for not too much more.

Passing intialization values to the React component

To pass in props to the React component from the server, you can take advantage of the Stimulus Values API.

In your view, you’d do something like:

.fancy-graph-container{ data: { controller: "fancyGraph", fancy_graph_units_value: current_user.preferred_units }}

In the example above, I’m imagining the Rails controller gives us access to a method current_user, which gets the currently logged in user, and allows us to get the user’s preferred units. Maybe they want to see all weight values in KG.

In your Stimulus controller, you’d do:

// skipping import statements

export default class extends Controller {
  static values = {
    units: String,
  };

  connect() {
    ReactDOM.render(
      <FancyGraphComponent weightUnits={this.unitsValue} />,
      this.element
    );
  }
}

And like that, you can now control how your component is rendered, based on application state.

Handling requests to the server

It is possible that your server needs to know about state changes in your React component, and then modify how the React component is rendered based on that information. Here’s one way you can hook that up.

Let’s say our Fancy Graph component has a toggle with which the user can set their preferred units. When the user hits the toggle, we want the component to tell our server about it.

The request you make is going to look something like:

fetch("/update_units", {
  method: "PATCH",
  body: JSON.stringify({ units: "lb" }),
  headers: {
    "X-CSRF-Token": csrfToken,
    "Content-Type": "application/json",
  },
});

The code above assumes you are protecting your requests from CSRF attacks, and therefore need to send in the right CSRF token to your server to ensure that Rails does not reject your request. If you haven’t seen it before, you should be able to navigate to any HTML page that your app serves and assuming you have protect_from_forgery with: :exception (or something like that) enabled on the controller action, you’ll see something similar in your HTML:

<head>
  <meta name="csrf-param" content="authenticity_token" />
  <meta
    name="csrf-token"
    content="FJ3PuXsj11s-tnjJx7eI9t-5mZdErhfXJDpjF3q3YhN4-b90gZLSAgSuLZs8Q6B9DvaxTLU7qnq_5naBSGY8eQ"
  />
</head>

So before we do the fetch, we could do:

const csrfToken = document
  .querySelector('meta[name="csrf-token"]')
  .getAttribute("content");

fetch(...)

Where you put the fetch is up to you. You could put it inside your React component, or you could pass in a callback from your Stimulus controller to your React component.

Things to watch out for

Conclusion

Try this out for yourself, and let me know how it goes! What do you like about the approach, and what do you feel are the drawbacks? Do you have any questions or did you run into any issues?

If you liked this article, consider subscribing to my email newsletter to get notified whenever I publish a new one.

Want to be notified when I publish a new article?