Modern apps have highly dynamic interfaces: API calls through Ajax for updating parts of it, dynamic forms like wizards with several steps, and even forms that perform API calls (think of searching something), and so go further and even have routing (our very website!). Some of these modern interfaces are built with React. We like React because it’s simple, and because these dynamic interfaces are easy to build. But the React way of doing things requires that you have a Frontend (obviously built in React, think of create-react-app
) and a Backend (that could be anything), ie, 2 separate parts of the app. Most React frontends are SPAs (Single Page Application).
In most of our apps, the main piece in our tech stack is Ruby on Rails. Ruby on Rails is great when it comes to shipping software fast. Especially when you are used to it and have a pretty solid background using it. But Rails is a full stack framework, meaning that all content is rendered in the server and then sent to the client. This is great for most of the cases, but something you require dynamic interfaces. The React way of doing things simply doesn’t match Rails’ standard approach. Yes, you can do an API-only rails app to serve as the React app’s backend. And yes, you can use React with Rails since Webpack is integrated into the framework. But doing this will add time (a lot) to the schedule. Remember: we want to ship software fast. Also, if the app you’re building consists of lots of CRUD views, doing all those views in React feels like overdoing it.
Our last couple of projects, for example, Dunu506 and Raive, had key features that needed to be highly dynamic. In Raive, there’s a complex campaign wizard that needs to communicate to Facebook. In Dunu506, there’s even a more complex ad wizard where the number of steps changes depending on your selections, or even different fields, etc. and there are forms that need to React (pun intended) to user interaction. We know firsthand that form wizards are a pain if you do full server-side apps, where each step is submitted, validated, and returned to the client with the new step, etc., so we knew that React was the way to do it.
So, how do we make React components without going full SPA? And how do we benefit from Rails? The answer is simple. In a React app, you have an App component that gets mounted on a root element in your view. We can do the same with any component and any element in our server-side rendered view.
For example:
<div id="hello" data-props="{ name: 'Patricio' }"></div>
and then in our app/javascript/packs/application.js
, we’d have something like:
import React from 'react'
import ReactDOM from 'react-dom'
import Hello from './Hello'
document.addEventListener('DOMContentLoaded', () => {
const div = document.querySelector('#hello')
if (div) {
const props = JSON.parse(div.getAttribute('data-props'))
ReactDOM.render(
<Hello {...props} />,
div
)
}
})
This code snippet worked fine for a while and we applied it to different parts of our projects. At first, the repetition of code began to be evident, so the first change to be made was to obtain a reusable function to mount the desired components.
import MyReactComponent from './MyReactComponent'
function mountComponent(id, Component) {
document.addEventListener('DOMContentLoaded', () => {
const div = document.querySelector(id)
if (div) {
const props = JSON.parse(div.getAttribute('data-props'))
ReactDOM.render(
<Component {...props} />,
div
)
}
})
}
mountComponent('#my-react-component', MyReactComponent)
With this change, we only had to make a call to the function for each component that we wanted to use, indicating its corresponding id
. But… What if we wanted to call the same component in different places in the application? Would it be correct to make different calls to the mountComponent()
function using different ids
for the same component? maybe yes, but for us, those questions are what led us to the next step, obtaining as a result a single function that mounts components in each section where they were defined, using as a convention the HTML id attribute for those components that are used only once and class for those that are reused (this decision was made to follow the same workflow between teammates), allowing to receive different data-props
between them.
<div id="my-react-component" data-props="{ name: 'John' }"></div>
or
<div class="my-react-component" data-props="{ name: 'John' }"></div>
import MyReactComponent from './MyReactComponent'
function mountComponent(selector, Component) {
document.addEventListener('DOMContentLoaded', () => {
const elements = document.querySelectorAll(selector)
elements.forEach(element => {
const props = JSON.parse(element.getAttribute('data-props'))
ReactDOM.render(
<Component {...props} />,
element
)
})
})
}
mountComponent('#my-react-component', MyReactComponent)
Now, only one call to the function is needed for all the id
or class
elements that are linked to the component.
Although this change seemed to be a good improvement, it did not seem correct for us to use the id
and class
attributes for indicating the use of one or multiple components because they could be required for different purposes, like styling with bootstrap or tailwind, another javascript calls, etc. (and it could end on a mess in the future). Therefore, after a short brainstorming process, we decided to use a new attribute, and we decided to call it data-component
. This attribute allowed us not to interfere with the use of id
and class
and at the same time it gave us the declaration about which component is being used in that HTML element (really good when debugging is needed! :) ).
As a result, we ended up with the following code:
<div id="someId" class="someClass" data-component="MyReactComponent" data-props="{ name: 'John' }"></div>
import MyReactComponent from './MyReactComponent'
function mountComponents (components) {
document.addEventListener('DOMContentLoaded', () => {
const roots = document.querySelectorAll('[data-component]')
Array.from(roots).forEach((root) => {
const props = JSON.parse(root.dataset.props)
const Component = components[root.dataset.component]
ReactDOM.render(
<Component {...props} />,
root
)
})
})
}
mountComponents({
MyReactComponent
})
At this point, we were very satisfied with the result: A clean way to mount any react component into rails views (but it can be used with other frameworks too) with a little effort. Also, this allowed the team to establish the way to do our projects. That was the moment when we decided to build this library.
Results
We came up with a very simple library that we use in all of our projects where react components are needed. We are very happy to share it with the community. It’s a library that facilitates our development, and maybe it could be useful for someone else. Feel free to collaborate with this project on GitHub!