Paweł Dąbrowski
Posted on August 10, 2022
More and more people are using Ruby on Rails to create a back-end API application for a front-end app.
But what if you want to create a rich and functional interface with JavaScript and use Rails for the back-end, without having them in separate repositories? You can create a monolithic Rails application.
This article will show you how to connect a Rails application with a front-end developed in React (without splitting the code into two separate applications). We'll also give some alternatives if React is not your framework of choice, but you like the idea of a monolithic app with a rich front-end.
But first, let’s start with an architecture overview to see why linking Rails and React is an option worth considering.
App Architecture: An Overview
A while back, when a developer started to build a web application, they didn’t have a list of possible architectures they could use. For the most part, web apps were monoliths without a very interactive user interface.
In modern software development, we have much more to choose from. We can:
- Use the old monolith architecture
- Go for a separated back-end and front-end
- Use service-oriented architecture
Let’s take a closer look at the most common types of architecture.
Headless Architecture
In headless architecture, the head of an application is detached from its body. In other words, you create a back-end application using Ruby, Python, Node.js, or another programming language. This application manages a connection with a database and provides computing power.
The body is a front-end application created with a framework like React, Vue, or Angular.
CDN stands for content delivery network, a service designed to deliver assets faster for visitors worldwide. We can build the architecture in the cloud environment, or each setup piece can be a separate server.
Choose headless architecture when:
- You aim for a large codebase. If your application is going to be very large, the separate back-end and front-end layers will make it more maintainable.
- You expect different workloads for different system elements. With a more modular approach, you can scale modules separately.
- You foresee that you'll change the technology in the future. It is easier to adopt new technology with a headless architecture, as the communication, in most cases, will be the same between the back-end and front-end, regardless of the stack.
Avoid a headless approach when:
- You want to develop an MVP version of an app very quickly. You will likely start over again in the future and consider a different angle.
- You have a very small team. Finding the right person to do the back-end, front-end, and DevOps stuff simultaneously might be hard.
- You want to build a straightforward application. Unfortunately, the headless approach increases your app's complexity.
Monolith Architecture
One application handles the presentation layer (front) and computation layer (back) in a monolith architecture. Such an approach speeds up the application creation process and simplifies the server setup.
As you'll notice, in a monolith, we eliminate a part of the communication that's in the headless approach. Such architecture improves the performance and security of an application.
Choose this architecture when:
- You develop an MVP version of your app. Frameworks like Ruby on Rails or Django make it easy and fast to build robust monolith applications.
- You start with a small team. Back-end developers can easily handle the front-end in such an architecture, and the deployment process is straightforward.
- Your application doesn’t have to depend on a very interactive front-end, and you don’t want to build a single-page application.
Avoid the monolith approach if:
- You know that the front-end of the application will be huge. It is harder to maintain this part as it's connected directly with the back-end codebase.
- You know the front-end will have a completely different workload than the back-end. As a result, you won’t scale certain elements of the monolith easily.
- You may change the front-end or back-end tech stack in the future. Such a transition would be pretty complicated with this architecture.
Hybrid Architecture
An alternative approach to the monolith and headless architectures is a hybrid. You create a monolith application, but instead of using the front-end produced by the back-end framework, use the technology of headless applications.
Yet this comes with a few disadvantages which you should consider:
- The codebase is shared. So as the application grows, it is harder to navigate through all the files and build a structure that is easy to understand.
- Scaling only the back-end or front-end is very difficult as both parts connect in one application.
- Being agile is not easy. With every change, you deploy the whole application, even if it’s just a change in the front-end style.
However, you also get some benefits that you wouldn’t get with a standard monolith architecture:
- You can work separately on the back-end and front-end and update libraries independently. Changing the stack of the front-end is easier.
- You can build a very interactive, single-page application based on the monolith with a reasonably simple deployment process.
- You can be flexible. It's possible to use the front-end served by the JavaScript framework for some parts of your application.
With these pros and cons of hybrid architecture in mind, we will now go through the design process of a monolith application (created with Rails and a front-end React framework).
Designing a Monolith with React
There are three main things to keep in mind before you build a monolith application with a React front-end:
- Front-end framework installation process - there are a few ways of adding React to a Rails application. However, each has some limitations, so choosing the right one for your project is essential.
- Communication inside the application - The back-end needs to expose data to the front-end, and the front needs to present this data. This element of the system needs to be designed carefully to make information exchange as smooth and fast as possible. As always, your choice should depend on the type of application you would like to build.
- Alternatives - You can still go for the hybrid approach but not use React. We will show you other solutions that you can quickly adapt to build a more interactive monolith.
Install React in Your Ruby on Rails App
There are three main ways to install React in your Rails application.
Install React Using Ruby Gems
We can extend the functionality of our app by installing external libraries called Ruby gems. If you are unfamiliar with JavaScript, this is the fastest way to add React to Rails.
The react-rails library is one of the most popular Ruby gems to integrate React with Rails. It provides generators for components, testing helpers, and view helpers to render JavaScript code inside the views.
If you accept another layer of logic for your front-end, using this Ruby gem is the best way to get started with React quickly. However, remember that besides the JS framework updates, you also have to depend on the gem updates. The more gems you use, the more problematic the Rails upgrade process can become.
Install React Using Import Maps
Import maps were first presented in Rails 7. The lib lets you build modern JS applications using libraries made for ES modules without transpiling or bundling.
Installing React with import maps is as easy as calling this command from the terminal:
$ ./bin/importmap pin react react-dom
You don’t even need to host source files on your server, as they are served from JavaScript’s CDN by default. By applying the --download
flag, you can download the files to the vendor directory inside your application.
However, you cannot use JSX (the most popular rendering extension in the React community) with import maps. HTM library is an alternative solution.
Install React Using Package Manager
This option seems to be the most flexible and optimal way to add React to a Rails project. You don’t create another level of abstraction for the front-end as you fetch the library directly. Updating React is also simpler.
The most popular package managers in the Rails community are NPM and Yarn. You can add React libraries by invoking this command in the terminal:
yarn add react react-dom # or npm install react react-dom
With this installation method, you won’t get any Rails helpers to render JavaScript code. However, we'll show you how to install and run React components using this approach shortly.
Communication Between Your Ruby on Rails App and React
Choosing the right way to exchange information between your Rails application and React components is essential. Selecting the wrong method can harm your application's performance and make your code less maintainable.
There are two common, standardized ways of exposing information from the back-end — REST and GraphQL. Let's take a look at both options.
REST Communication
The REST API is a pretty simple way of exchanging information. We have different request types at our disposal to perform CRUD (create, retrieve, update, delete) operations. If you are familiar with the concept of resources in Rails, the REST API works the same way.
You call the /authors
endpoint if you want to pull information about authors or a single author. You call the /posts
endpoint to do the same, but for posts. If a single page in your application requires a lot of different data, you will perform multiple HTTP requests.
You should consider using REST in your application if:
- Your front-end application doesn’t need to render data from different sources on a single page. On the other hand, this doesn’t have to mean that the application is very simple.
- You don’t want to implement the cache mechanism on the application level. Using REST API, you can benefit from the HTTP cache provided by browsers.
- You care about error reporting. Implementing an error report with REST is easy as you work on different HTTP responses. With GraphQL, it’s not, as your application always returns a 200 response status.
GraphQL Communication
GraphQL represents an entirely different approach to data fetching. Using this technology, you perform one request and get only the data you need. You can hit multiple endpoints at once and pull only one attribute per endpoint.
With GraphQL implemented on the back-end, you always call the same endpoint and modify the query parameter.
You should consider using GraphQL in your application if:
- You are building a very interactive application requiring the front-end to pull a lot of different data at once.
- You want to build your application very quickly. Building an API with REST is slower. With GraphQL, you get maximum flexibility, and all you need to care about in every step is the content of the query.
- You are building an application where the API is used by multiple different applications like mobile, front-end, or services.
Front-end Alternatives to React for Your Ruby on Rails App
If you don’t want to use React but like the idea of having a front-end application inside the Rails monolith, there are some alternative solutions available.
Rails Turbo
Turbo allows you to write single-page applications without the need for any JavaScript code. You design the page from independent frames. They behave like components and can also be lazy-loaded if needed.
With Rails Turbo, you don’t spend time building the JSON responses from an API. You can simply reuse Rails views and render HTML — it’s automatically transferred and rendered in your components. This reduces the amount of code in your application.
You should consider using Rails Turbo if:
- You don’t like writing JavaScript. It is possible to design rich and interactive web applications without the need for a single line of JavaScript.
- You like the Rails framework. The Turbo library provides helper methods to quickly build interactive pages or transform existing ones into more dynamic forms.
- You want to move fast. As mentioned before, you don’t have to care about the JSON responses from an API. You can simply serve HTML views and the Turbo library will automatically pull them.
Stimulus JS
Stimulus is a JavaScript framework created by the Rails team. It was designed to extend the HTML that you already have. The main concept of this framework is the controller — one controller per view.
Your Rails application will automatically load the JavaScript controller for the given view and map the HTML elements. You can respond to events or modify the view when you need to.
You should consider using Stimulus if:
- You are already using Rails Turbo, but want more control over a page's granular elements.
- You don’t want to build a single-page application but still want to make some page elements interactive or dynamic.
Other JavaScript Frameworks
Besides React, other interesting JavaScript frameworks on the market right now include Vue, Svelte, and Ember.js. So if you like writing JavaScript components, you can choose one of these.
Rails Turbo and/or Stimulus JS are the perfect choice if you're a Rails developer who's familiar with JavaScript but doesn’t want to write much of it. Both libraries heavily follow the principles of the Rails framework, so if you already work in this ecosystem, it will be easier for you to use these extensions.
Build a Monolith with React
It’s time to build a simple Rails application to manage a list of books we want to read. Let's start with generating the Rails skeleton.
Ruby on Rails Project Bootstrap
Ensure you have the latest version of Ruby on Rails installed locally on your operating system. Then generate the skeleton using the rails new
command:
rails new booklist -d postgresql -j esbuild
The above command will generate a new Rails project in the booklist
directory, with support for a PostgreSQL database. We can now enter the project’s directory, create the database, and run the server:
cd booklist/
./bin/rails db:create
rails s
Visit the localhost:3000 address in your browser to see the default Rails welcome screen.
Now generate a home controller for a single endpoint in the application so we can attach the React application to the layout. Run this command in the terminal:
./bin/rails g controller Home index
The last step is to update config/routes.rb
file to set the root path:
Rails.application.routes.draw do
root "home#index"
end
We can now focus on the front-end part with React.
React Installation
We will install the react
, react-dom
, and node-uuid
libraries using yarn in our terminal:
yarn add react react-dom node-uuid
To properly handle JSX, we also have to update our build script. First, open the package.json
file, and in the scripts
section, ensure that the build
command is the following:
esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --loader:.js=jsx
You can now run ./bin/dev
to kickstart both yarn build
and the rails server or run them separately. For example, if you want processes in separate terminal tabs, run rails s
in one and yarn build –watch
in the second.
Render a React Application in a Rails Layout
We want to run the React application when a user enters the main root in our Rails application. To do this, update the app/views/layout.html.erb
file, so that the body
section looks like this:
<body>
<div id="app"></div>
</body>
The React application will render the component with JavaScript logic inside the app
div.
Let’s now create the entry point for the React application. Open the app/javascript/application.js
file and write the following code:
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("app");
const root = createRoot(container);
root.render(<App />);
We have the container element, but we are missing the App
component so the application will throw an error. Let’s create the App
component then.
Writing a React Component
We won’t create a separate directory for components. Let's place our tiny application in one component for demonstration purposes.
Note: When writing a production app, always design the React app using multiple components and follow the best design principles.
Create a file App.js
in the app/javascript
directory and place the application skeleton there, with the header as a placeholder:
import React, { useState } from "react";
export default function App() {
const [books, setBooks] = useState([]);
return (
<>
<h1>Books {books.length}</h1>
</>
);
}
Rendering a Book List
Now it’s time to render a simple list of books. To do this, we have to update the render
function:
return (
<>
<h1>Books {books.length}</h1>
<div>
<table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
{books &&
books.map(({ id, title, author }, i) => (
<tr key={id}>
<td>{title}</td>
<td>{author}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
Create a New Book
We don’t have anything we can render. Let’s add the form to create a new book so we can fill our list with some data that we can later edit and delete if needed.
However, before we do this, we have to update our component to store some more information:
import { default as UUID } from "node-uuid";
const [action, setAction] = useState("list");
const [formData, setFormData] = useState({ title: "", author: "" });
I’m not going to create a separate component for the form, so I have to use a conditional in the render function. Thanks to the action
, I can tell when to render the list or form.
Our render
function is the following:
return (
<>
<h1>Books {books.length}</h1>
{action == "list" ? (
<div>
<button onClick={() => setAction("form")}>New book</button>
<table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th></th>
</tr>
</thead>
<tbody>
{books &&
books.map(({ id, title, author }, i) => (
<tr key={id}>
<td>{title}</td>
<td>{author}</td>
<td></td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div>
<form>
<label>Title:</label>
<input
onChange={(e) =>
setFormData({ ...formData, title: e.target.value })
}
name="title"
value={formData.title}
/>
<label>Author:</label>
<input
onChange={(e) =>
setFormData({ ...formData, author: e.target.value })
}
name="author"
value={formData.author}
/>
<button onClick={(e) => saveBook(e)}>Submit</button>
<button onClick={() => setAction("list")}>Back</button>
</form>
</div>
)}
</>
);
Each time you type something into the title or author input, formData
will update with values from the form. The last missing piece is the saveBook
function:
const saveBook = (e) => {
e.preventDefault();
setBooks([...books, { ...formData, id: UUID.v4() }]);
setFormData({ title: "", author: "", id: "" });
setAction("list");
};
The first step is to prevent a form submission — we don’t want to reload the page. Then we update the books collection and reset the form data. The last step is to render back the list view.
Update an Existing Book
Let's reuse the form we created in the previous step. We also have to store the id of the book we are currently editing:
const [currentBookId, setCurrentBookId] = useState(null);
The editBook
function will set the id of the book we want to edit. Fill the form with values and render the form view:
const editBook = (id) => {
const currentBook = books.find((book) => book.id == id);
setCurrentBookId(id);
setFormData({
...formData,
title: currentBook.title,
author: currentBook.author,
});
setAction("form");
};
Since we are going to use the saveBook
function for a creation and update action, we have to modify it accordingly:
const saveBook = async (e) => {
e.preventDefault();
if (currentBookId) {
bookIndex = books.findIndex((book) => book.id == currentBookId);
updatedBooks = [...books];
updatedBooks[bookIndex] = formData;
setBooks(updatedBooks);
setCurrentBookId(null);
} else {
setBooks([...books, { ...formData, id: UUID.v4() }]);
}
setFormData({ title: "", author: "", id: "" });
setAction("list");
};
Delete a Single Book
Let’s start with building the simple code for destroying the book:
const deleteBook = (id) => {
setBooks(books.filter((book) => book.id != id));
};
We get the array of books that aren't deleted and update the books
collection. React will update our component automatically.
The Complete Component
We implemented all the actions needed to create, update, list, and destroy a book. View the code for the complete component in this Gist.
Recap
Congratulations, you created a React application inside a Rails monolith! Let’s summarize what we have learned and done during this article.
Different Types of Architecture for Web Apps
When you build a web application, it has a front and back part. So you need to decide how you want to connect these two sides. Here's a reminder of the most common types of architecture:
- Headless architecture - The front-end application is separate from the back-end. Choose it if you expect to write a lot of code, want to change technologies in the future, or expect a different workload for the front-end and back-end. However, keep in mind that such architecture increases the complexity of an application, takes more time to build, and the development process might be complex for a small development team.
- Monolith architecture - One application that handles both front-end and back-end. You can build one with popular frameworks like Ruby on Rails or Django. With this architecture, you can build fast with a small team. However, you can run into trouble in the future if you want to change the technology or scale only part of the application.
- Hybrid architecture - A compromise between a monolith and headless architecture. You build one codebase, but the front-end is served by a different technology from the back-end. The codebase might be hard to navigate, and it will be challenging to scale only one piece of the system. However, it is more flexible than a monolith. If you don’t like monoliths, but don’t want to fully detach the front from the back-end, this solution is for you.
The Design Process to Build Hybrid Architecture
Don’t jump into coding straight away. Think about the specifications of your application. When building a Rails application that uses a React framework for the front-end, you have to consider the following things:
- Installation process - You can install React using import maps if you don’t need to use JSX templates. Otherwise, you can select package managers like Yarn or NPM, or install React using a Ruby gem.
- Communication between Rails and React - Both applications are inside the same codebase, but need a bridge to exchange information. Use REST API for simpler applications and GraphQL for more complex interfaces where a lot of different data is necessary for the view layer.
Alternatives to React for a Rails App
If you decide that you don’t want to use React, but you like the idea of hybrid architecture, you can consider the following alternative solutions for the front-end:
- Rails Turbo - With this library, you can split the page into independent frames and update them when needed. Go for this option if you like the Rails framework, and don’t want to write a lot of JavaScript code.
- Stimulus JS - A small library for the HTML you already have in your views. Choose it when you are happy with your Rails monolith, but you want to make some views more interactive.
- Other JavaScript frameworks - If you want to write more JavaScript, you can use Vue.js, Svelte, or Ember.js, instead of React. You can easily incorporate these solutions into your Rails application.
Summing Up
In this post, we explored the three main types of architecture — headless, monolith, and hybrid — and looked at their pros and cons. We then created a React application inside a Rails monolith.
Now it should hopefully be easier for you to choose the right architecture and tools for your next project.
Until next time, happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Posted on August 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.