Rails 7 Using React With Htm
Published: Feb 18, 2022
Last updated: Feb 18, 2022
Over the next two posts, I will be showing you two ways to setup a React frontend setup with your new Rails application:
- A Node-less setup using import map to bring in
htm
andreact
. - 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
- Basic familiarity with Bundler.
- Basic familiarity with setting up a new Rails project.
- 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:
- Creating a new Rails project called
demo-importpin-react-rails-7
. - Creating a
app/javascript/components/index.js
file andapp/javascript/components/htm_create.js
file. These files will be used to add ourhtm
-style React component and render helper. - Creating a controller called
components
and aindex
action. This will be added to ourroutes.rb
as our a root route. - Pinning the
htm
,react
andreact-dom
dependencies usingimportmap-rails
. By default, this will pin the latest version of each dependency from thejspm
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
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
Rails 7 Using React With Htm
Introduction