React on Rails: File-System-Based Automated Bundle Generation

react on railsOctober 19, 2022Dotby Pulkit

File-System-Based Bundle Generation in React on Rails

Superior performance on any webpage requires loading the minimum amount of JavaScript needed. Shakapacker, via webpack 5, automatically splits the JavaScript code into bundles that load only the required components for a page.

React on Rails 13.1.0 version introduced the File-System-Based Bundle Generation feature to minimize the work needed to configure a page to load only the required JavaScript bundles based on the page's React components. The system supports complex cases of many React components within layout or content partials.

If you're new and want to see this in action, skip to the File-Based Bundle Generation section. If you're upgrading a project using Webpacker or Shakapacker, maybe with or without React on Rails, the following section explains the problem of accidentally including too much JavaScript.

Single Output bundle

Webpack prepares JavaScript code and other static assets, like stylesheets, images, and fonts, into bundle files for loading by the browser.

The Shakapacker gem enables convenient integration of Webpack into the Rails development flow.

The following scenario will present the problem and the solution.

Given this webpacker.yml file:

default: &default
  source_path: app/javascript
  source_entry_path: packs
  public_root_path: public
  public_output_path: packs

Consider a simple Rails application for call logs that has three components CallLogs, Navigation, and Home components.

app/javascript:
  └── packs:               # sets up webpack entries
  │   └── application.js   # references Home.jsx, CallLogs.jsx and Navigation.jsx in `../src`
  └── src:
  │   └── home
  │   │   └── ...
  │   │   └── Home.jsx
  │   └── dashboard
  │   │   └── ...
  │   │   └── CallLogs.jsx
  │   │   └── Navigation.jsx
  └── stylesheets:
  │   └── homePage.css
  └── images:
      └── logo.svg

In the above case, application.js under the app/javascript/packs directory is the single entry point for webpack. It registers the components Home, CallLogs, and Navigation.

import ReactOnRails from "react-on-rails"
import Home from "../src/home/Home"
import CallLogs from "../src/dashboard/CallLogs"
import Navigation from "../src/dashboard/Navigation"

ReactOnRails.register({
  Home,
  CallLogs,
  Navigation,
})

The javascript_pack_tag helper adds the script tag.

# app/views/layouts/application.html.erb

<%= javascript_pack_tag "application" %>

What if the CallLogs component of the application is rarely used? Everybody that loads the Home component still downloads the CallLogs component. Unnecessarily loading JavaScript increases the page load time, takes more memory, and lowers PageSpeed scores, hurting SEO performance. Ideally, we want to load the CallLogs component when one navigates there.

Code-Splitting

Webpack's code-splitting splits the JavaScript code into several bundle files. For the above example, we need to load Home only when a user navigates to the home page, not the dashboard pages. We can create separate bundles for these pages by creating multiple entry points in the packs directory as follow:

app/javascript:
  └── packs:
# Registers Home Component using ReactOnRails.register
  │   └── homepage-bundle.jsx
# Registers CallLogs & Navigation Component using ReactOnRails.register
  │   └── dashboard-bundle.jsx
  └── src:
  │   └── home
  │   │   └── ...
  │   │   └── Home.jsx
  │   └── dashboard
  │   │   └── ...
  │   │   └── CallLogs.jsx
  │   │   └── Navigation.jsx
  └── stylesheets:
  │   └── homePage.css
  └── images:
      └── logo.svg

Shakapacker's append_stylesheet_pack_tag and append_javascript_pack_tag view helper enables the Rails partials and main view to specify the bundles needed. These calls must be done before the call to stylesheet_pack_tag and javascript_pack_tag in order for them to work correctly. Your Rails views, including partials, already specify calls to react_component and react_component_hash, specifying what components to display. Could we skip the extra work of calls to append_stylesheet_pack_tag and append_javascript_pack_tag, eliminating a source of errors? Yes!

File-Based Bundle Generation

File-System-Based Bundle Generation removes the need to add components in the packs directory explicitly. It automatically detects and registers the component for usage in the Rails view.

Configuration
  1. Enable nested_entries

To use this feature, React on Rails will automatically create a generated subdirectory inside the packs directory for the components that must be registered for usage on Rails View/Partials.

Thus, set nested_entries: true in the webpacker.yml as below to enable a subdirectory for entry points.

default: &default
  source_path: app/javascript
  source_entry_path: packs
  public_root_path: public
  public_output_path: packs
  nested_entries: true
  1. Configure Components Subdirectory

components_subdirectory is the name of the directories containing components to be registered for use in the Rails view helpers. Thus, we can use these components in the react_component and react_component_hash view helper methods without configuring the bundle inclusion on the page.

Set the name of the components subdirectory in our config/initializers/react_on_rails file as below:

config.components_subdirectory = "ror_components"
  1. Configure auto_load_bundle Option

auto_load_bundle option in config/initializers/react_on_rails configures the default value that gets passed to Rails view helpers react_component and react_component_hash.

config.auto_load_bundle = true

The default value is false. Alternatively, the parameter can be specified when calling react_component and react_component_hash—implementation

  1. Remove parameters passed to javascript_pack_tag and stylesheet_pack_tag, which previously contained the components.

    The updated layout file gets updated to:

    <%= javascript_pack_tag %>
    <%= stylesheet_pack_tag %>
  1. Remove calls to append_javascript_pack_tag and append_stylesheet_pack_tag from the views and partials. \

  2. Configure .gitignore to exclude the packs/generated directory

    For each component in the ror_components subdirectory, React on Rails creates a generated pack in the packs/generated directory. We want git to ignore such files. \

The app/javascript directory now looks like this. Note, ror_components is configured in your config.react_on_rails.rb file.

app/javascript:
└── packs
└── src:
│ └── home
│ │ └── ror_components
│ │ └── Home.jsx
│ └── dashboard
│ │ └── ror_components
│ │ │ └── CallLogs.jsx
│ │ │ └── Navigation.jsx

With these changes, there is no need to register these React components or directly add their bundles. We can use these components in the Rails view without any additional work.

<%= react_component("Home", {}, auto_load_bundle: true) %>
<%= react_component("CallLogs", {}, auto_load_bundle: true) %>
<%= react_component("Navigation", {}, auto_load_bundle: true) %>
Server Rendering and Client Rendering Components

If server-side rendering is enabled, the components get registered for usage on both client and server rendering. We can define two files, one for server-side and client-side as ComponentName.server.jsx and ComponentName.client.jsx respectively.

Adding File-System-Based Automated Bundle Generation to an Existing Project

To add the automated bundle generation feature incrementally, set the auto_load_bundle configuration to false. When calling the react_component or react_component_hash helpers, pass the auto_load_bundle option as true.

Please check out this PR and docs to learn more about this feature.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right