Back to home

Rails 7 Using React With Htm main image

Rails 7 Using React With Htm

Over the next two posts, I will be showing you two ways to setup a React frontend setup with your new Rails application:

  1. A Node-less setup using import map to bring in htm and react.
  2. A Node setup using esbuild (that will support TypeScript out of the box).

It is worth noting upfront that today's post using htm is not necessarily my preference and changes how one would write React, but I felt it was worth exploring and sharing.

Source code can be found here

Prerequisites

  1. Basic familiarity with Bundler.
  2. Basic familiarity with setting up a new Rails project.
  3. Not necessary, but we will be adding JavaScript dependencies using importmap-rails.

Getting started

Assuming you are using Rails 7.x, we will be using the following command to install the necessary dependencies:

# Setup $ rails new demo-importpin-react-rails-7 $ cd demo-importpin-react-rails-7 # Creating the necessary files for the project $ mkdir app/javascript/components $ touch app/javascript/components/index.js app/javascript/components/htm_create.js # Setting up a controller to render the React app $ ./bin/rails g controller components index # Pinning the dependencies that we will be using $ ./bin/importmap htm react react-dom

In the above, we are doing the following:

  1. Creating a new Rails project called demo-importpin-react-rails-7.
  2. Creating a app/javascript/components/index.js file and app/javascript/components/htm_create.js file. These files will be used to add our htm-style React component and render helper.
  3. Creating a controller called components and a index action. This will be added to our routes.rb as our a root route.
  4. Pinning the htm, react and react-dom dependencies using importmap-rails. By default, this will pin the latest version of each dependency from the jspm CDN.

At this , we need to update our config.routes.rb file to use our new controller index method components#index as the root route:

Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Defines the root path route ("/") root "components#index" end

Finally, our config/importmap.rb needs to look like this:

# Pin npm packages by running ./bin/importmap pin 'application', preload: true pin '@hotwired/turbo-rails', to: 'turbo.min.js', preload: true pin '@hotwired/stimulus', to: 'stimulus.min.js', preload: true pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js', preload: true pin_all_from 'app/javascript/controllers', under: 'controllers' pin 'react', to: 'https://ga.jspm.io/npm:react@17.0.2/index.js' pin 'react-dom', to: 'https://ga.jspm.io/npm:react-dom@17.0.2/index.js' pin 'object-assign', to: 'https://ga.jspm.io/npm:object-assign@4.1.1/index.js' pin 'scheduler', to: 'https://ga.jspm.io/npm:scheduler@0.20.2/index.js' pin_all_from 'app/javascript/components', under: 'components'

Our ./bin/importmap command early should have added most of the commands we need, but ensure that you have added pin_all_from 'app/javascript/components', under: 'components' at the bottom.

At this stage, our app is setup to be able to handle our React app.

Updating our application.js file

At this point, we need to update our app/javascript/application.js file to reflect that we want to resolve the app/javascript/components directory that we created during setup:

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails"; // import "controllers"; import "components";

At this point, we can now update our app/javascript/components/index.js file as our basic React app that is resolved by the application file:

But before we do that, we want to make our life a little bit easier by creating a help for htm to handle the binding of our React.createElement function to a tagged template string of our React component.

The htm createElement helper

Inside of app/javascript/components/htm_create_element.js, we will create a helper that will be used to create our React component:

import { createElement } from "react"; import htm from "htm"; export default htm.bind(createElement);

You can read the basics of htm on GitHub, but the basic example it gives on how things work is the following:

import htm from "htm"; function h(type, props, ...children) { return { type, props, children }; } const html = htm.bind(h); console.log(html`<h1 id="hello">Hello world!</h1>`); // { // type: 'h1', // props: { id: 'hello' }, // children: ['Hello world!'] // }

In our case with React, we wanted to pass the createElement function to htm.bind instead of the example h function provided above. This will become our helper.

Note: although the approach we wrote with the helper is more verbose, it also looks like you could bypass what we are about to do and use import { html } from 'htm/react'; instead. See usage for more.

At this stage, we are ready to use our helper to render our React app.

Updating our application code

Back in app/javascript/components/index.js, we can now add the following:

import { render } from "react-dom"; import h from "components/htm_create_element"; const App = () => { return h`<div>Hello, Rails 7 Importpin!</div>`; }; render(h`<${App} />`, document.getElementById("root"));

Here, we are importing in our helper h which will enable us to render our React app.

As you will see with the above code, it moves away from the standard React syntax you would be familiar with as all the component code needs with be added as a tagged template to the h function.

We are now ready to set up our view to render the app.

Rendering the final app

Update the app/views/components/index.html.erb generated during our set up to have the following:

<h1>Components#index</h1> <div id="root"></div>

As denoted in our application code from the previous section, we are rendering the app on the #root element.

If we now run our Rails server using bin/rails s, we can head to our localhost:3000 page and we should see our React app.

React app

React app

Awesome! Our app is setup and ready to go.

Summary

Today's post demonstrated how to setup a Node-less version of React to run with Rails 7.

This required the use of importpin-rails and htm as a dependency. It also required the use of tagged templates, which is a departure from those familiar with React syntax.

In my personal opinion, I do not enjoy the idea of using tagged templates for a larger application, but in the next post I will be exploring a setup of Rails 7 with ESBuild that will bring us back to familiar territory.

Resources and further reading

Photo credit: thebrownspy

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.