| slug | auto-bundling |
|---|
To use the automated bundle generation feature introduced in React on Rails v13.1.0, please upgrade to use Shakapacker v6.5.1 at least. If you are currently using Webpacker, please follow the migration steps available v6 upgrade. Then upgrade to Shakapacker 7 using v7 upgrade guide.
To use the automated bundle generation feature, set nested_entries: true in the shakapacker.yml file like this.
The generated files will go in a nested directory.
default:
...
nested_entries: trueFor more details, see Configuration and Code section in shakapacker.
Example (dummy app):
nested_entries: truewith a differentsource_path: client/app. Seeconfig/shakapacker.ymlin the dummy app. Dummy shakapacker.yml
components_subdirectory is the name of the matched directories containing components that will be automatically registered for use by the view helpers.
For example, configure config/initializers/react_on_rails to set the name for components_subdirectory:
config.components_subdirectory = "ror_components"Now all React components inside the directories called ror_components will automatically be registered for usage with react_component and react_component_hash helper methods provided by React on Rails.
Example (dummy app): the configured components subdirectory is named
startupinstead ofror_components. Dummy initializer
For automated component registry, react_component and react_component_hash view helper method tries to load generated bundle for component from the generated directory automatically per auto_load_bundle option. auto_load_bundle option in config/initializers/react_on_rails configures the default value that will be passed to component helpers. The default is false, and the parameter can be passed explicitly for each call.
You can change the value in config/initializers/react_on_rails by updating it as follows:
config.auto_load_bundle = trueExample (dummy app):
auto_load_bundleis set totruein the same initializer. Dummy initializer
Generated files will go to the following two directories:
- Pack files for entrypoint components will be generated in the
{Shakapacker.config.source_entry_path}/generateddirectory. - The interim server bundle file, which is only generated if you already have a server bundle entrypoint and have not set
make_generated_server_bundle_the_entrypointtotrue, will be generated in the{Pathname(Shakapacker.config.source_entry_path).parent}/generateddirectory.
To avoid committing generated files to your version control system, please update .gitignore to include:
# Generated React on Rails packs
**/generated/**If you already have an existing server bundle entrypoint and have not set make_generated_server_bundle_the_entrypoint to true, then pack generation will add an import statement to your existing server bundle entrypoint similar to:
// import statement added by react_on_rails:generate_packs rake task
import './../generated/server-bundle-generated.js';We recommend committing this import statement to your version control system.
Example (dummy app): see the server bundle entrypoint import. Dummy server-bundle.js
If the shakapacker.yml file is configured as instructed in the Shakapacker configuration guide, with the following configurations
default: &default
source_path: app/javascript
source_entry_path: packs
public_root_path: public
public_output_path: packs
nested_entries: true
# And morethe directory structure will look like this
app/javascript:
└── packs: # sets up webpack entries
│ └── application.js # references FooComponentOne.jsx, BarComponentOne.jsx and BarComponentTwo.jsx in `../src`
└── src: # any directory name is fine. Referenced files need to be under source_path
│ └── Foo
│ │ └── ...
│ │ └── FooComponentOne.jsx
│ └── Bar
│ │ └── ...
│ │ └── BarComponentOne.jsx
│ │ └── BarComponentTwo.jsx
└── stylesheets:
│ └── my_styles.css
└── images:
└── logo.svg
Previously, many applications would use one pack (webpack entrypoint) for many components. In this example, theapplication.js file manually registers server components, FooComponentOne, BarComponentOne and BarComponentTwo.
import ReactOnRails from 'react-on-rails';
import FooComponentOne from '../src/Foo/FooComponentOne';
import BarComponentOne from '../src/Foo/BarComponentOne';
import BarComponentTwo from '../src/Foo/BarComponentTwo';
ReactOnRails.register({ FooComponentOne, BarComponentOne, BarComponentTwo });Your layout would contain:
<%= javascript_pack_tag 'application' %>
<%= stylesheet_pack_tag 'application' %>The single-pack approach above loads every component's JavaScript on every page, even pages that only render one or two components. To avoid shipping unused code, you can split the bundle manually by giving each component its own pack file. Each pack file imports one component and registers it — webpack treats each pack as a separate entry point and produces a separate bundle.
app/javascript/
├── packs/ # each file here is a webpack entry point
│ ├── FooComponentOne.jsx # imports FooComponentOne, calls ReactOnRails.register({ FooComponentOne })
│ ├── BarComponentOne.jsx # imports BarComponentOne, calls ReactOnRails.register({ BarComponentOne })
│ └── BarComponentTwo.jsx # imports BarComponentTwo, calls ReactOnRails.register({ BarComponentTwo })
└── src/ # shared source; any directory name works as long as it's under source_path
├── Foo/
│ └── FooComponentOne.jsx # the actual component implementation
└── Bar/
├── BarComponentOne.jsx
└── BarComponentTwo.jsx
Each pack file is typically a one-liner that imports the component and registers it:
// app/javascript/packs/FooComponentOne.jsx
import ReactOnRails from 'react-on-rails';
import FooComponentOne from '../src/Foo/FooComponentOne';
ReactOnRails.register({ FooComponentOne });The tricky part with this manual approach is telling each Rails view which bundles to load. Shakapacker's append_javascript_pack_tag and append_stylesheet_pack_tag view helpers let each view declare the packs it needs, which the layout's javascript_pack_tag and stylesheet_pack_tag calls then render.
Auto-bundling (described in the next section) automates both steps: it generates these per-component pack files for you, and the react_component view helper automatically appends the right pack tags at render time.
File-system-based automated pack generation simplifies this process with a new option for the view helpers.
Note: In the Background examples above, we used
BarComponentTwo. In the Solution below, we refer to the same component asSpecialComponentNotToAutoLoadBundleto emphasize that it is excluded from auto-loading. You do not need to rename your files.
For example, if you wanted to utilize our file-system based entrypoint generation for FooComponentOne and BarComponentOne, but not SpecialComponentNotToAutoLoadBundle (formerly BarComponentTwo) (for whatever reason), then...
-
Remove generated entrypoints from parameters passed directly to
javascript_pack_tagandstylesheet_pack_tag. -
Remove generated entrypoints from parameters passed directly to
append_javascript_pack_tagandappend_stylesheet_pack_tag.Your layout would now contain:
<%= javascript_pack_tag('SpecialComponentNotToAutoLoadBundle') %> <%= stylesheet_pack_tag('SpecialComponentNotToAutoLoadBundle') %>
-
Create a directory structure where the components that you want to be auto-generated are within
ReactOnRails.configuration.components_subdirectory, which should be a subdirectory ofShakapacker.config.source_path:app/javascript: └── packs: │ └── SpecialComponentNotToAutoLoadBundle.jsx # Internally uses ReactOnRails.register └── src: │ └── Foo │ │ └── ... │ │ └── ror_components # configured as `components_subdirectory` │ │ └── FooComponentOne.jsx │ └── Bar │ │ └── ... │ │ └── ror_components # configured as `components_subdirectory` │ │ │ └── BarComponentOne.jsx │ │ └── something_else │ │ │ └── SpecialComponentNotToAutoLoadBundle.jsx-
You no longer need to register the React components within the
ReactOnRails.configuration.components_subdirectorynor directly add their bundles. For example, you can have a Rails view using three components:<%= react_component("FooComponentOne", {}, auto_load_bundle: true) %> <%= react_component("BarComponentOne", {}, auto_load_bundle: true) %> <% append_javascript_pack_tag('SpecialComponentNotToAutoLoadBundle') %> <%= react_component("SpecialComponentNotToAutoLoadBundle", {}) %>
If a component uses multiple HTML strings for server rendering, the
react_component_hashview helper can be used on the Rails view, as illustrated below.<% foo_component_one_data = react_component_hash( "FooComponentOne", prerender: true, auto_load_bundle: true, props: {} ) %> <% content_for :title do %> <%= foo_component_one_data["title"] %> <% end %> <%= foo_component_one_data["componentHtml"] %>
The default value of the
auto_load_bundleparameter can be specified by settingconfig.auto_load_bundleinconfig/initializers/react_on_rails.rband thus removed from each call toreact_component.
-
When using auto_load_bundle: true, your Rails layout needs to include empty pack tag placeholders where React on Rails will inject the component-specific CSS and JavaScript bundles automatically:
<!DOCTYPE html>
<html>
<head>
<!-- Your regular head content -->
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<!-- Empty pack tags - React on Rails injects component CSS/JS here -->
<%= stylesheet_pack_tag %>
<%= javascript_pack_tag %>
</head>
<body>
<%= yield %>
</body>
</html>How it works:
-
Component calls automatically append bundles: When you use
<%= react_component("ComponentName", props, auto_load_bundle: true) %>in a view, React on Rails automatically callsappend_javascript_pack_tag "generated/ComponentName"andappend_stylesheet_pack_tag "generated/ComponentName"(in static/production modes). -
Layout renders appended bundles: The empty
<%= stylesheet_pack_tag %>and<%= javascript_pack_tag %>calls in your layout are where the appended component bundles get rendered. -
No manual bundle management: You don't need to manually specify which bundles to load - React on Rails handles this automatically based on which components are used in each view.
Example with multiple components:
If your view contains:
<%= react_component("HelloWorld", @hello_world_props, auto_load_bundle: true) %>
<%= react_component("HeavyMarkdownEditor", @editor_props, auto_load_bundle: true) %>React on Rails automatically generates HTML equivalent to:
<!-- In <head> where <%= stylesheet_pack_tag %> appears -->
<%= stylesheet_pack_tag "generated/HelloWorld" %>
<%= stylesheet_pack_tag "generated/HeavyMarkdownEditor" %>
<!-- Before </body> where <%= javascript_pack_tag %> appears -->
<%= javascript_pack_tag "generated/HelloWorld" %>
<%= javascript_pack_tag "generated/HeavyMarkdownEditor" %>This enables optimal bundle splitting where each page only loads the CSS and JavaScript needed for the components actually used on that page.
Here's a step-by-step example showing how to set up file-system-based automated bundle generation from scratch:
In config/shakapacker.yml:
default: &default
source_path: app/javascript
source_entry_path: packs
public_root_path: public
public_output_path: packs
nested_entries: true # Required for auto-generation
cache_manifest: falseIn config/initializers/react_on_rails.rb:
ReactOnRails.configure do |config|
config.components_subdirectory = "ror_components" # Directory name for auto-registered components
config.auto_load_bundle = true # Enable automatic bundle loading
config.server_bundle_js_file = "server-bundle.js"
endSet up your directory structure like this:
app/javascript/
└── src/
├── HelloWorld/
│ ├── HelloWorld.module.css # Component styles
│ └── ror_components/ # Auto-registration directory
│ └── HelloWorld.jsx # React component
└── HeavyMarkdownEditor/
├── HeavyMarkdownEditor.module.css # Component styles
└── ror_components/ # Auto-registration directory
└── HeavyMarkdownEditor.jsx # React component
app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx:
import React from 'react';
import styles from '../HelloWorld.module.css';
const HelloWorld = ({ name }) => (
<div className={styles.hello}>
<h1>Hello {name}!</h1>
<p>Welcome to React on Rails with auto-registration!</p>
</div>
);
export default HelloWorld;app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.jsx:
import React, { useState, useEffect } from 'react';
import styles from '../HeavyMarkdownEditor.module.css';
const HeavyMarkdownEditor = ({ initialContent = '# Hello\n\nStart editing!' }) => {
const [content, setContent] = useState(initialContent);
const [ReactMarkdown, setReactMarkdown] = useState(null);
const [remarkGfm, setRemarkGfm] = useState(null);
// Dynamic imports for SSR compatibility
useEffect(() => {
const loadMarkdown = async () => {
const [{ default: ReactMarkdown }, { default: remarkGfm }] = await Promise.all([
import('react-markdown'),
import('remark-gfm'),
]);
setReactMarkdown(() => ReactMarkdown);
setRemarkGfm(() => remarkGfm);
};
loadMarkdown();
}, []);
if (!ReactMarkdown) {
return <div className={styles.loading}>Loading editor...</div>;
}
return (
<div className={styles.editor}>
<div className={styles.input}>
<h3>Markdown Input:</h3>
<textarea value={content} onChange={(e) => setContent(e.target.value)} className={styles.textarea} />
</div>
<div className={styles.output}>
<h3>Preview:</h3>
<div className={styles.preview}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
</div>
</div>
);
};
export default HeavyMarkdownEditor;app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html>
<head>
<title>React on Rails Auto-Registration Demo</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<!-- Empty pack tags - React on Rails injects component CSS/JS here -->
<%= stylesheet_pack_tag %>
<%= javascript_pack_tag %>
</head>
<body>
<%= yield %>
</body>
</html>app/controllers/hello_world_controller.rb:
class HelloWorldController < ApplicationController
def index
@hello_world_props = { name: 'Auto-Registration' }
end
def editor
@editor_props = {
initialContent: "# Welcome to the Heavy Editor\n\nThis component demonstrates:\n- Dynamic imports for SSR\n- Bundle splitting\n- Automatic CSS loading"
}
end
endapp/views/hello_world/index.html.erb:
<%= react_component("HelloWorld", @hello_world_props, prerender: true) %>app/views/hello_world/editor.html.erb:
<%= react_component("HeavyMarkdownEditor", @editor_props, prerender: true) %>Run the pack generation command:
bundle exec rake react_on_rails:generate_packsThis creates:
app/javascript/packs/generated/HelloWorld.jsapp/javascript/packs/generated/HeavyMarkdownEditor.js
# Generated React on Rails packs
**/generated/**Now when you visit your pages, React on Rails automatically:
- Loads only the CSS and JS needed for components on each page
- Registers components without manual
ReactOnRails.register()calls - Enables optimal bundle splitting and caching
Bundle sizes in this example (measured from browser dev tools):
- HelloWorld: 1.1MB total resources (50KB component-specific code + shared React runtime)
- HelloWorld.js: 10.0 kB
- HelloWorld.css: 2.5 kB
- Shared runtime: ~1.1MB (React, webpack runtime)
- HeavyMarkdownEditor: 2.2MB total resources (2.7MB with markdown libraries)
- HeavyMarkdownEditor.js: 26.5 kB
- HeavyMarkdownEditor.css: 5.5 kB
- Markdown libraries: 1,081 kB additional
- Shared runtime: ~1.1MB (React, webpack runtime)
Bundle splitting benefit: Each page loads only its required components - the HelloWorld page doesn't load the heavy markdown libraries, saving ~1.1MB (50% reduction)!
HelloWorld (Lightweight Component):

HeavyMarkdownEditor (Heavy Component):

Screenshots show browser dev tools network analysis demonstrating the dramatic difference in bundle sizes and load times between the two components.
If server rendering is enabled, the component will be registered for usage both in server and client rendering. To have separate definitions for client and server rendering, name the component files ComponentName.server.jsx and ComponentName.client.jsx. The ComponentName.server.jsx file will be used for server rendering and the ComponentName.client.jsx file for client rendering. If you don't want the component rendered on the server, you should only have the ComponentName.client.jsx file.
Example (dummy app): paired files such as
ReduxApp.client.jsxandReduxApp.server.jsx, andRouterApp.client.jsxandRouterApp.server.jsx.
Once generated, all server entrypoints will be imported into a file named [ReactOnRails.configuration.server_bundle_js_file]-generated.js, which in turn will be imported into a source file named the same as ReactOnRails.configuration.server_bundle_js_file. If your server bundling logic is such that your server bundle source entrypoint is not named the same as your ReactOnRails.configuration.server_bundle_js_file and changing it would be difficult, please let us know.
Important
When specifying separate definitions for client and server rendering, you need to delete the generalized ComponentName.jsx file.
Important
Not related to React Server Components. The .client. and .server. file suffixes control which webpack bundle imports the file — a React on Rails auto-bundling concept that predates React Server Components. If you are using RSC, classification of a component as a server component is controlled separately by the 'use client' directive, and the rules for using these suffixes with RSC are covered in Auto-Bundling with React Server Components below.
Pro Feature. React Server Components (RSC) require React on Rails Pro with the Node renderer. This section assumes you already have RSC enabled in your project — see Create a React Server Component and Preparing your app for RSC for setup.
When React Server Components are enabled, the auto-bundling system understands two kinds of components and chooses the correct registration function for each:
- Server Components — files without a
'use client'directive at the top. Their code runs only on the server; their output is serialized to an RSC payload. - Client Components — files with a
'use client'directive at the top. Their code runs in the browser (and during SSR) and hydrates as normal React components.
The presence or absence of 'use client' is the only signal the generator uses to classify a file as a server or client component. File suffixes (.client.jsx / .server.jsx) have a different meaning (see below).
For every file inside components_subdirectory, the generator:
- Reads the file contents and checks for a
'use client'directive on the first non-comment line. - If the directive is present → generates a client-bundle pack that imports the component and calls
ReactOnRails.register({ ComponentName }), and imports the component into the aggregated server bundle withReactOnRails.register({ ComponentName }). - If the directive is absent → generates a client-bundle pack that calls
registerServerComponent("ComponentName")(without importing the component), and imports the component into the aggregated server bundle withregisterServerComponent({ ComponentName }).
You don't call registerServerComponent yourself when using auto-bundling — the generator writes these calls for you based on the classification above.
Important
If you forget the 'use client' directive on a component that needs it, the generator classifies the file as a Server Component and wires it up with registerServerComponent. The packs generator does scan for common client-side patterns (hooks like useState, useEffect; event handlers like onClick, onChange; class components extending React.Component) and logs a warning if it detects them in a file classified as a Server Component. However, not all client-only code triggers this heuristic — always double-check that client components start with 'use client'.
registerServerComponent has two different shapes depending on which bundle you import it from:
| Bundle | Import | Signature | Why |
|---|---|---|---|
| Client | react-on-rails-pro/registerServerComponent/client |
registerServerComponent(...names: string[]) |
Takes only component names because the server component's code stays on the server. The client fetches the RSC payload when the component renders (or uses the payload already embedded in the HTML if the page was server-rendered). |
| Server | react-on-rails-pro/registerServerComponent/server |
registerServerComponent(components: { [name]: Component }) |
Takes an object with the actual component references because the code needs to be bundled into the server and RSC bundles for RSC payload generation. |
Auto-bundling uses both forms under the hood: the per-component client packs use the client form, and the aggregated server bundle file uses the server form.
A typical components_subdirectory for an RSC-enabled project mixes server and client components in the same directory:
app/javascript/src/
└── ror_components/
├── Dashboard.jsx # Server Component (no 'use client')
├── Profile.jsx # Server Component (no 'use client')
├── LikeButton.jsx # Client Component ('use client' at top)
└── CommentForm.jsx # Client Component ('use client' at top)
Each file produces its own generated pack, and pages only load the packs for the components they render. You don't need to separate server and client components into subdirectories — the 'use client' directive is the classifier, not the location.
For the layout above, running bin/rails react_on_rails:generate_packs produces:
Per-component client packs (one webpack entry point each):
// packs/generated/Dashboard.js (server component — no component import)
import registerServerComponent from 'react-on-rails-pro/registerServerComponent/client';
registerServerComponent('Dashboard');// packs/generated/LikeButton.js (client component — imported and registered normally)
import ReactOnRails from 'react-on-rails-pro/client';
import LikeButton from '../../ror_components/LikeButton.jsx';
ReactOnRails.register({ LikeButton });Because each is a separate entry point, a view that calls react_component("LikeButton", ...) only loads the LikeButton pack; it never loads the Dashboard pack or any code for server components it doesn't render.
Aggregated server bundle file (one file for all components):
// generated/server-bundle-generated.js
import ReactOnRails from 'react-on-rails-pro';
import Dashboard from '../ror_components/Dashboard.jsx';
import Profile from '../ror_components/Profile.jsx';
import LikeButton from '../ror_components/LikeButton.jsx';
import CommentForm from '../ror_components/CommentForm.jsx';
import registerServerComponent from 'react-on-rails-pro/registerServerComponent/server';
registerServerComponent({ Dashboard, Profile });
ReactOnRails.register({ LikeButton, CommentForm });Your existing packs/server-bundle.js entry file doesn't need manual changes — the packs generator adds one import line at the top pointing to the aggregated file:
// packs/server-bundle.js
import './../generated/server-bundle-generated.js'; // added by react_on_rails:generate_packs
// ... your own custom server-side code continues hereThe .client.jsx / .server.jsx suffixes that auto-bundling supports for splitting client components into separate client-side and server-side rendering definitions are unrelated to the 'use client' directive. The suffixes control which webpack bundle imports the file; the directive controls whether the file is a React Server Component. These are orthogonal concepts.
Rules for using variants with RSC:
-
Never apply
.client/.serversuffixes to actual server components. A server component's code exists only in the RSC bundle — there is no client-side version to split.Dashboard.jsxshould stay asDashboard.jsx, not becomeDashboard.client.jsxorDashboard.server.jsx. Using these suffixes on a server component causes the wrong variant (the server one) to be loaded in the browser via the client manifest. See troubleshooting item #8. -
Use variants only for client components that need different client-side vs server-side rendering logic — for example, a client component that uses
BrowserRouterin the browser andStaticRouterduring SSR. In that case, both.client.tsxand.server.tsxmust start with'use client', since both are client components (they just use different imports). -
Use variants for the
wrapServerComponentRendererwrapper pattern. When a client component containsRSCRouteand needs to be wrapped withwrapServerComponentRenderer, the wrappers are authored as.client.tsxand.server.tsxvariants. See Embedding Server Components in Client Components for the full walkthrough.
- Create a React Server Component — step-by-step tutorial for setting up RSC and authoring your first server component.
- Preparing your app for RSC — infrastructure setup when migrating an existing app to RSC.
- RSC Component Patterns — migration patterns for restructuring your component tree as you adopt RSC.
- Embedding Server Components in Client Components — the
RSCRoute+wrapServerComponentRendererpattern for rendering server components inside a client component tree. - RSC Troubleshooting — common errors and fixes.
Components compiled by transpiled languages like ReScript produce output files with extensions that include the transpiler identifier (e.g., .bs.js, .res.js). Auto-bundling discovers these files because they end in .js, but it extracts the component name from the full extension — so MyComponent.bs.js becomes MyComponent.bs instead of MyComponent.
If you see errors like:
Could not find component registered with name MyComponent.bs- Component renders as
MyComponent.bsinstead ofMyComponentin error messages
Then you likely need the wrapper pattern described below.
The simplest solution is a thin wrapper file in your ror_components directory:
app/javascript/src/Comments/
├── ReScriptShow.bs.js # ReScript compiler output
└── ror_components/
└── ReScriptShow.jsx # Wrapper for auto-registration
// app/javascript/src/Comments/ror_components/ReScriptShow.jsx
import ReScriptShow from '../ReScriptShow.bs.js';
export default ReScriptShow;This pattern works for any transpiled language and requires no gem configuration changes. The wrapper file can use .js, .jsx, .ts, or .tsx depending on your project setup.
Note
While it's possible to add gem-level configuration for additional extensions, the wrapper-file pattern is recommended because it:
- Works immediately with no configuration changes
- Makes the component registration explicit and visible in the file tree
- Avoids coupling your build pipeline to gem internals that may change between versions
As of version 13.3.4, bundles inside directories that match config.components_subdirectory will be automatically added as entrypoints, while bundles outside those directories need to be manually added to the Shakapacker.config.source_entry_path or Webpack's entry rules.
Note
Most applications use React components without Redux. If you don't use Redux stores, you can skip this section entirely.
In addition to components, React on Rails can automatically register Redux stores based on file system conventions. This eliminates manual ReactOnRails.registerStore() calls and generates individual packs for each store. The feature works the same way as component auto-registration.
Add stores_subdirectory to your React on Rails initializer:
config.stores_subdirectory = "ror_stores"This requires auto_load_bundle to be enabled (either globally or per helper call) and nested_entries: true in shakapacker.yml, just like component auto-registration.
Place store files inside directories matching your configured stores_subdirectory:
app/javascript/
└── src/
├── Comments/
│ └── ror_stores/
│ └── commentsStore.js
└── Router/
└── ror_stores/
└── routerStore.ts # TypeScript is supported
Each store file must export a store generator function as its default export:
// app/javascript/src/Comments/ror_stores/commentsStore.js
import { createStore } from 'redux';
import commentsReducer from '../reducers/commentsReducer';
const commentsStore = (props, railsContext) => {
return createStore(commentsReducer, props);
};
export default commentsStore;Use the redux_store helper with auto_load_bundle:
<%# The store pack is loaded automatically when auto_load_bundle is enabled %>
<%= redux_store("commentsStore", props: { comments: @comments }) %>
<%= react_component("CommentsContainer", auto_load_bundle: true) %>If auto_load_bundle is set globally to true, you can omit it from each call:
<%= redux_store("commentsStore", props: { comments: @comments }) %>
<%= react_component("CommentsContainer") %>Running bundle exec rake react_on_rails:generate_packs generates:
Client pack (app/javascript/packs/generated/commentsStore.js):
import ReactOnRails from 'react-on-rails/client';
import commentsStore from '../../src/Comments/ror_stores/commentsStore.js';
ReactOnRails.registerStore({ commentsStore });Server bundle — stores are also automatically included in the generated server bundle, so server-side rendering with Redux works without manual configuration.
Component and store names must be unique across both registries. If a component and a store share the same name, pack generation will raise an error:
**ERROR** ReactOnRails: The following names are used for both components and stores: myName.
This would cause pack file conflicts in the generated directory.
Please rename your components or stores to have unique names.
Duplicate store names (two store files with the same name in different directories) are also detected and raise an error.
Like components, store files support .client and .server suffixes. For example, commentsStore.client.js will only be used for client-side rendering, and the suffix is stripped from the registered name.
Problem: react_component helper throws "Component not found" error.
Solutions:
- Ensure your component is in a
ror_componentsdirectory (or your configuredcomponents_subdirectory) - Run
rake react_on_rails:generate_packsto generate the component bundles - Check that your component exports a default export:
export default MyComponent; - Verify the component name matches the directory structure
- If using a transpiled language (ReScript, Reason, etc.), see Transpiled Languages — files like
MyComponent.bs.jsregister asMyComponent.bsinstead ofMyComponent
Problem: Components load but CSS styles are missing or delayed, particularly with server-side rendering and auto_load_bundle = true.
Root Cause: When using auto_load_bundle = true, react_component calls automatically invoke append_stylesheet_pack_tag during rendering. However, Shakapacker requires these appends to execute BEFORE the main stylesheet_pack_tag in your layout's <head>. Since Rails renders the layout's <head> before the <body> (where react_component calls typically occur), the appends happen too late, causing FOUC.
Solution: Use the content_for :body_content pattern documented in Shakapacker's Preventing FOUC guide.
This pattern renders your body content first, ensuring all react_component auto-appends execute before the <head> renders:
<%# Step 1: This block executes first, capturing content AND triggering auto-appends %>
<% content_for :body_content do %>
<%= react_component "NavigationBarApp", prerender: true %>
<div class="container">
<%= yield %>
</div>
<%= react_component "Footer", prerender: true %>
<% end %>
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%# Step 2: Head renders with all accumulated stylesheet/JS appends %>
<%= stylesheet_pack_tag(media: 'all') %>
<%= javascript_pack_tag(defer: true) %>
</head>
<body>
<%# Step 3: Finally, the captured body_content is rendered here %>
<%= yield :body_content %>
</body>
</html>Note: While defining body content before <!DOCTYPE html> may seem counter-intuitive, Rails processes the content_for block first (capturing content and triggering appends), then renders the HTML in proper document order.
Alternative: If the content_for pattern doesn't fit your needs, disable auto-loading and manually specify packs:
# config/initializers/react_on_rails.rb
config.auto_load_bundle = falseAdditional Resources:
- Complete FOUC prevention guide: Shakapacker Preventing FOUC documentation
- Turbo/Hotwire integration: Using auto-bundling with Turbo - same pattern required for Turbo compatibility
- Working example: react-webpack-rails-tutorial PR #686
- Related issue: Shakapacker #720
Note: HMR-related FOUC in development mode (dynamic CSS injection) is separate from this SSR auto-loading issue. See Shakapacker docs for details.
Note: If you're using Tailwind CSS or another utility-first framework and see layout jumps (sidebars collapsing, flex containers stacking), you may need to inline critical CSS. See Tailwind/Utility-First CSS FOUC for the solution.
Problem: Server-side rendering fails with browser-only API access.
Solutions:
- Use dynamic imports for browser-only libraries:
useEffect(() => { const loadLibrary = async () => { const { default: BrowserLibrary } = await import('browser-library'); setLibrary(() => BrowserLibrary); }; loadLibrary(); }, []);
- Provide fallback/skeleton components during loading
- Consider client-only rendering: use
ComponentName.client.jsxfiles only
Problem: Running rake react_on_rails:generate_packs doesn't create files.
Solutions:
- Verify
nested_entries: trueinshakapacker.yml - Check that
components_subdirectoryis correctly configured - Ensure components are in the right directory structure:
src/ComponentName/ror_components/ComponentName.jsx - Make sure you're using the correct source path in Shakapacker config
Problem: Manually specified javascript_pack_tag or stylesheet_pack_tag break.
Solutions:
- Remove specific pack names from manual pack tags: use
<%= javascript_pack_tag %>instead of<%= javascript_pack_tag 'specific-bundle' %> - Remove manual
append_javascript_pack_tagcalls -react_componentwithauto_load_bundle: truehandles this automatically - Delete any client bundle entry files (e.g.,
client-bundle.js) that manually register components
Problem: Large bundles loading when not needed.
Solutions:
- Use component-level bundle splitting - each page loads only needed components
- Implement dynamic imports for heavy dependencies
- Check bundle analysis with
RAILS_ENV=production NODE_ENV=production bundle exec rails assets:precompileand examine generated bundle sizes - Consider code splitting within heavy components
Problem: Works in development but fails in production.
Solutions:
- CSS: Production extracts CSS to separate files, development might inline it
- Source maps: Check if source maps are causing issues in production
- Minification: Some code might break during minification - check console for errors
- Environment: Use
bin/dev prodto test production-like assets locally
Problem: A component with .server.jsx/.client.jsx file variants is rendered through RSCRoute or registered with registerServerComponent. This adds unnecessary overhead and can cause the wrong variant (the server version) to load in the browser — the RSC bundle imports the .server.jsx file and its chunk reference propagates through the client manifest to the browser.
See When to use .client / .server variants with RSC for the full rules on when these suffixes are and aren't appropriate in an RSC project.
Solution depends on the component's intended role:
If the component is a client component (uses hooks, state, event handlers, or browser APIs):
- Ensure it has the
'use client'directive so it gets registered viaReactOnRails.register()instead ofregisterServerComponent() - Do not render it through
RSCRoute— usereact_componentorstream_react_componentinstead - The
.server/.clientvariant pattern works correctly withreact_componentandstream_react_componentas long as the component is properly registered as a client component
If the component should be a server component (server-side data fetching, no client interactivity):
- Migrate to a single file — delete the
.client.jsxfile, since server components do not run on the client side at all - Adjust the
.server.jsxvariant (or rename it to drop the.serversuffix) to properly use RSC capabilities: async data fetching, streaming with Suspense, direct access to server-only resources - If the component uses a render function pattern (
export default (props, railsContext) => { ... }), convert it to a plain React component (export default function Component(props) { ... }) or an async component (export default async function Component(props) { ... }) - Remove the
'use client'directive if present — server components must not have it
Related: GitHub issue #2509, How to use different files for client and server rendering
To debug auto-loading behavior, temporarily add logging to see what bundles are being loaded:
<!-- Temporarily add this to your layout to see what gets loaded -->
<%= debug(content_for(:javascript_pack_tags)) %>
<%= debug(content_for(:stylesheet_pack_tags)) %>This helps verify that components are correctly appending their bundles.