Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 385 additions & 0 deletions docs/guides/file-system-based-automated-bundle-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,276 @@ For example, if you wanted to utilize our file-system based entrypoint generatio

The default value of the `auto_load_bundle` parameter can be specified by setting `config.auto_load_bundle` in `config/initializers/react_on_rails.rb` and thus removed from each call to `react_component`.

### Layout Integration with Auto-Loading

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:

```erb
<!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>
```
Comment on lines +224 to +242
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Place JS placeholder before for consistency and performance.

The text later claims JS renders “before ,” but the example shows it in . Move javascript_pack_tag to just before and keep CSS in .

   <!-- Empty pack tags - React on Rails injects component CSS/JS here -->
-  <%= stylesheet_pack_tag %>
-  <%= javascript_pack_tag %>
+  <%= stylesheet_pack_tag %>
 </head>
 <body>
   <%= yield %>
+  <!-- JS placeholder at the end of body -->
+  <%= javascript_pack_tag %>
 </body>
🤖 Prompt for AI Agents
In docs/guides/file-system-based-automated-bundle-generation.md around lines 224
to 242, the example shows the javascript_pack_tag in the head but the text
states JS renders "before </body>"; move the javascript_pack_tag out of the head
and place it just before the closing </body> tag while leaving the
stylesheet_pack_tag in the head, and update the example markup accordingly so
the JS placeholder appears immediately before </body>.


**How it works:**

1. **Component calls automatically append bundles**: When you use `<%= react_component("ComponentName", props, auto_load_bundle: true) %>` in a view, React on Rails automatically calls `append_javascript_pack_tag "generated/ComponentName"` and `append_stylesheet_pack_tag "generated/ComponentName"` (in static/production modes).

2. **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.

3. **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:
```erb
<%= 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:
```erb
<!-- 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.

## Complete Working Example

Here's a step-by-step example showing how to set up file-system-based automated bundle generation from scratch:

### 1. Configure Shakapacker

In `config/shakapacker.yml`:

```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: false
```

### 2. Configure React on Rails

In `config/initializers/react_on_rails.rb`:

```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"
end
```

### 3. Directory Structure

Set up your directory structure like this:

```text
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
```

### 4. Component Implementation

`app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx`:

```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`:

```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;
```

### 5. Rails Layout

`app/views/layouts/application.html.erb`:

```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>
```

### 6. Rails Views and Controller

`app/controllers/hello_world_controller.rb`:

```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
end
```

`app/views/hello_world/index.html.erb`:

```erb
<%= react_component("HelloWorld", @hello_world_props, prerender: true) %>
```

`app/views/hello_world/editor.html.erb`:

```erb
<%= react_component("HeavyMarkdownEditor", @editor_props, prerender: true) %>
```

### 7. Generate Bundles

Run the pack generation command:

```bash
bundle exec rake react_on_rails:generate_packs
```

This creates:
- `app/javascript/packs/generated/HelloWorld.js`
- `app/javascript/packs/generated/HeavyMarkdownEditor.js`

### 8. Update .gitignore

```gitignore
# Generated React on Rails packs
**/generated/**
```

### 9. Start the Server

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)!

Comment on lines +469 to +481
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bundle size numbers are inconsistent; re-measure or generalize.

Values listed (e.g., “HelloWorld 1.1MB total” vs per-file sizes; “HeavyMarkdownEditor 2.2MB total (2.7MB with markdown libraries)”) conflict. This can mislead readers.

Proposed simplification:

-**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)!
+**Example bundle size guidance (varies by app and build settings):**
+- HelloWorld: small component bundle + shared runtime.
+- HeavyMarkdownEditor: larger component bundle + markdown libs + shared runtime.
+
+Tip: Measure with a production build and your browser’s Network panel; avoid quoting exact sizes unless measured in this repo/revision.

To re-measure precisely in your app:

#!/bin/bash
RAILS_ENV=production NODE_ENV=production bundle exec rails assets:precompile
echo "Bundle sizes under public/packs:"
# List top bundles and sizes
find public/packs -type f -maxdepth 2 -print0 | xargs -0 du -h | sort -h | tail -n 30

#### Performance Screenshots

**HelloWorld (Lightweight Component):**
![HelloWorld Bundle Analysis](../images/bundle-splitting-hello-world.png)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that this image is missing


**HeavyMarkdownEditor (Heavy Component):**
![HeavyMarkdownEditor Bundle Analysis](../images/bundle-splitting-heavy-markdown.png)

*Screenshots show browser dev tools network analysis demonstrating the dramatic difference in bundle sizes and load times between the two components.*

### Server Rendering and Client Rendering 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.
Expand All @@ -233,3 +503,118 @@ Once generated, all server entrypoints will be imported into a file named `[Reac
### Using Automated Bundle Generation Feature with already defined packs

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.

## Troubleshooting

### Common Issues and Solutions

#### 1. "Component not found" errors

**Problem**: `react_component` helper throws "Component not found" error.

**Solutions**:
- Ensure your component is in a `ror_components` directory (or your configured `components_subdirectory`)
- Run `rake react_on_rails:generate_packs` to generate the component bundles
- Check that your component exports a default export: `export default MyComponent;`
- Verify the component name matches the directory structure

#### 2. CSS not loading (FOUC - Flash of Unstyled Content)

**Problem**: Components load but CSS styles are missing or delayed.

**Important**: FOUC (Flash of Unstyled Content) **only occurs with HMR (Hot Module Replacement)**. Static and production modes work perfectly without FOUC.

**Solutions**:
- **Development with HMR** (`./bin/dev`): FOUC is expected behavior due to dynamic CSS injection - **not a bug**
- **Development static** (`./bin/dev static`): No FOUC - CSS is extracted to separate files like production
- **Production** (`./bin/dev prod`): No FOUC - CSS is extracted and optimized
- **Layout**: Verify your layout includes empty `<%= stylesheet_pack_tag %>` placeholder for CSS injection
- **Component imports**: Check that CSS files are properly imported: `import styles from './Component.module.css';`

**Key insight**: Choose your development mode based on your current needs:
- Use HMR for fastest development (accept FOUC)
- Use static mode when testing styling without FOUC
- Use production mode for final testing

#### 3. "document is not defined" errors during SSR

**Problem**: Server-side rendering fails with browser-only API access.

**Solutions**:
- Use dynamic imports for browser-only libraries:
```jsx
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.jsx` files only

#### 4. Bundles not being generated

**Problem**: Running `rake react_on_rails:generate_packs` doesn't create files.

**Solutions**:
- Verify `nested_entries: true` in `shakapacker.yml`
- Check that `components_subdirectory` is 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

#### 5. Manual pack tags not working after switching to auto-loading

**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_tag` calls - `react_component` with `auto_load_bundle: true` handles this automatically
- Delete any client bundle entry files (e.g., `client-bundle.js`) that manually register components

#### 6. Bundle size issues

**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:precompile` and examine generated bundle sizes
- Consider code splitting within heavy components

#### 7. Development vs Production differences

**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 prod` to test production-like assets locally

#### 8. Installation order issues

**Problem**: React on Rails installation fails or behaves unexpectedly.

**Solutions**:
- **Correct order**: Always install Shakapacker BEFORE React on Rails
```bash
bundle add shakapacker
rails shakapacker:install
bundle add react_on_rails
rails generate react_on_rails:install
```
- If you installed in wrong order, uninstall and reinstall in correct sequence

### Debug Mode

To debug auto-loading behavior, temporarily add logging to see what bundles are being loaded:

```erb
<!-- 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.
Binary file added docs/images/bundle-splitting-heavy-markdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading