React on Rails: File-System-Based Automated Bundle Generation
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
- 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
- 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"
- 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
-
Remove parameters passed to
javascript_pack_tag
andstylesheet_pack_tag
, which previously contained the components.The updated layout file gets updated to:
<%= javascript_pack_tag %>
<%= stylesheet_pack_tag %>
-
Remove calls to
append_javascript_pack_tag
andappend_stylesheet_pack_tag
from the views and partials. \ -
Configure
.gitignore
to exclude thepacks/generated
directoryFor each component in the
ror_components
subdirectory, React on Rails creates a generated pack in thepacks/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.