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: packsConsider 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.svgIn 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.svgShakapacker'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_bundleOption
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 = trueThe default value is false. Alternatively, the parameter can be specified when calling react_component and react_component_hash
-
Remove parameters passed to
javascript_pack_tagandstylesheet_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_tagandappend_stylesheet_pack_tagfrom the views and partials. \ -
Configure
.gitignoreto exclude thepacks/generateddirectoryFor each component in the
ror_componentssubdirectory, React on Rails creates a generated pack in thepacks/generateddirectory. 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.