Implementing your own helper to render React.js components on Ruby on Rails ERB templates
Recently I had the opportunity to use Ruby on Rails to develop an internal system for the company I work. It was a good exercise to remember how productive this framework can be, and how single page applications are not always the best choice for every problem.
The project has only one specific screen which has lots of user interaction without reloading, so we can use view libraries like React only on this specific screen, and leave Rails to manage all the other pages which are mostly CRUDs.
Initially, I decided to implement the interactions of this screen with jQuery solely, but it soon started to become complex, and that’s when React came into play.
Since webpacker was already present in the project, the only step I had to do to add React.js and JSX was to run rails webpacker:install:react
to install the dependencies as described in their documentation.
Now, the next step was to render my React component on the interactive screen. There is a gem called React-Rails which allows us to include the component inside your ERB template by calling a helper function like:
<%= react_component('Hello', name: 'World') %>
…where Hello
is the component name and {name: 'World'}
is the props object passed to the component. Under the hood, this will make the server render a div
tag with the component data (name and props) inside, and React-Rails will search for these tags on the client to mount the components in place when the DOM is ready.
React-Rails also has other features like server-side rendering, component rendering inside controllers, etc. Since the only feature I really needed from it was the view helper, I decided to leave it outside this project and implement the Rails helper and component-mounting JavaScript code on my own as an exercise.
Implementing the helper
The helper is the easiest part with only one line of method implementation. Just add a file react_helper.rb
in app/helpers
with the following code.
module ReactHelper
def react_component(name, props = {})
content_tag(:div, '', data: { react_component: { name: name, props: props } })
end
end
This will add a react_component
helper to your project and make it available to all your ERB templates. The helper will transform the Hello
component example I shown earlier in something like:
<div data-react-component='{"name":"Hello","props":{"name":"World"}}'></div>
Now, we need to write the code that will search for these tags and mount the React components on the browser when the page loads.
Implementing the component-mounting JavaScript code
First, we need to define where the React components are going to be located in our project. Just as in the React-Rails gem, we’ll put them on app/javascript/components
. Here’s the component used on the Hello
example shown previously:
app/javascript/components/Hello.jsx
import React from 'react'
const Hello = props => (
<div>Hello {props.name}!</div>
)
export default Hello
The next step is to write the code that will load and mount the components based on the tags generated by the helper. It will be placed under app/javascript/packs/react.js
.
Let’s use require.context
from Webpack to iterate over the components
folder and mount an object where the keys are the component names (extracted from the file names), and the values are the React components (exported by the component files).
// This will be the object populated with the components
const components = {}
// Here, we load the files from the `app/javascript/components` folder
const context = require.context('../components', false, /\.jsx$/)
for (const key of context.keys()) {
// Extract component name from the file path with a regular expression
// `../components/Hello.jsx` becomes `Hello`
const componentName = key.match(/([^/.]+)\.jsx/)[1]
// e.g. components['Hello'] = Hello component definition
components[componentName] = context(key).default
}
Now, let’s iterate over the div
tags generated by the react_component
Rails helper we’ve implemented previously and mount the components.
document.addEventListener('DOMContentLoaded', () => {
// Get all tags generated by the `rails_component` helper.
const mountingElements = document.querySelectorAll('[data-react-component]')
for (const element of mountingElements) {
// Extract name and props from the data present in the element
const { name, props } = JSON.parse(element.getAttribute('data-react-component'))
// Retrieve the component from the `components` object we mounted before,
// and check if it exists.
const component = components[name]
if (!component) {
console.log(`Component ${name} not found`)
continue
}
// Mount the component with the props inside the element
ReactDOM.render(
React.createElement(component, props || {}),
element
)
}
})
Here’s the full file with the ReactDOM inclusion.
app/javascript/packs/react.js
import React from 'react'
import ReactDOM from 'react-dom'
// This will be the object populated
const components = {}
// Here, we load the files from the `app/javascript/components` folder
const context = require.context('../components', false, /\.jsx$/)
for (const key of context.keys()) {
// Extract component name from the file path with a regular expression
// `../components/Hello.jsx` becomes `Hello`
const componentName = key.match(/([^/.]+)\.jsx/)[1]
// e.g. components['Hello'] = Hello component definition
components[componentName] = context(key).default
}
document.addEventListener('DOMContentLoaded', () => {
// Get all tags generated by the `rails_component` helper.
const mountingElements = document.querySelectorAll('[data-react-component]')
for (const element of mountingElements) {
// Extract name and props from the data present in the element
const { name, props } = JSON.parse(element.getAttribute('data-react-component'))
// Retrieve the component from the `components` object we mounted before,
// and check if it exists.
const component = components[name]
if (!component) {
console.log(`Component ${name} not found`)
continue
}
// Mount the component with the props inside the element
ReactDOM.render(
React.createElement(component, props || {}),
element
)
}
})
We need to include app/javascript/packs/react.js
into our application layout by adding the following code inside the head
tag.
<%= javascript_pack_tag 'react' %>
Testing it
Now, let’s finally test it.
config/routes.rb
Rails.application.routes.draw do
root to: 'home#index'
end
app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
end
end
app/views/home/index.html.erb
<h1>My page</h1>
<%= react_component('Hello', name: 'World') %>
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>My project</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<%= javascript_pack_tag 'application' %>
<%= javascript_pack_tag 'react' %>
</head>
<body>
<div class="container page-content">
<%= yield %>
</div>
</body>
</html>
And here we go!