Skip to content

Commit c522a46

Browse files
justin808claude
andauthored
docs: Improve file-system-based bundle generation guide (#1766)
* docs: Improve file-system-based bundle generation guide Add comprehensive improvements to the file-system-based automated bundle generation documentation to address gaps that cause confusion for both human developers and AI coding agents. ## Key improvements: ### 1. Layout Integration Clarification - Added detailed section explaining empty pack tag placeholders in Rails layouts - Clarified how react_component helper automatically calls append_javascript_pack_tag - Explained the connection between component usage and automatic bundle injection - Showed the complete flow from component rendering to CSS/JS loading ### 2. Complete Working Example - Added step-by-step setup guide from scratch - Included real component implementations with CSS modules and dynamic imports - Provided complete Rails controller, view, and layout code examples - Demonstrated proper directory structure with concrete file paths - Showed bundle splitting benefits with actual size comparisons (50KB vs 2.7MB) ### 3. Comprehensive Troubleshooting Section - Documented all common issues encountered during implementation - Added solutions for FOUC handling across different development modes - Covered SSR compatibility issues with dynamic imports - Included installation order requirements (Shakapacker before React on Rails) - Provided debug techniques for troubleshooting bundle loading - Addressed bundle size optimization and development vs production differences ### 4. Enhanced Developer Experience - Clear explanations of "why" behind configuration choices - Explicit file naming conventions and patterns - Debug mode instructions for investigating bundle loading issues - Common failure modes and their solutions These improvements make the documentation significantly more practical and reduce the likelihood of implementation errors. The additions are based on real-world implementation experience and address the most common stumbling blocks. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Improve docs --------- Co-authored-by: Claude <[email protected]>
1 parent 0e826ac commit c522a46

File tree

2 files changed

+385
-0
lines changed

2 files changed

+385
-0
lines changed

docs/guides/file-system-based-automated-bundle-generation.md

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,276 @@ For example, if you wanted to utilize our file-system based entrypoint generatio
219219
220220
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`.
221221
222+
### Layout Integration with Auto-Loading
223+
224+
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:
225+
226+
```erb
227+
<!DOCTYPE html>
228+
<html>
229+
<head>
230+
<!-- Your regular head content -->
231+
<%= csrf_meta_tags %>
232+
<%= csp_meta_tag %>
233+
234+
<!-- Empty pack tags - React on Rails injects component CSS/JS here -->
235+
<%= stylesheet_pack_tag %>
236+
<%= javascript_pack_tag %>
237+
</head>
238+
<body>
239+
<%= yield %>
240+
</body>
241+
</html>
242+
```
243+
244+
**How it works:**
245+
246+
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).
247+
248+
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.
249+
250+
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.
251+
252+
**Example with multiple components:**
253+
254+
If your view contains:
255+
```erb
256+
<%= react_component("HelloWorld", @hello_world_props, auto_load_bundle: true) %>
257+
<%= react_component("HeavyMarkdownEditor", @editor_props, auto_load_bundle: true) %>
258+
```
259+
260+
React on Rails automatically generates HTML equivalent to:
261+
```erb
262+
<!-- In <head> where <%= stylesheet_pack_tag %> appears -->
263+
<%= stylesheet_pack_tag "generated/HelloWorld" %>
264+
<%= stylesheet_pack_tag "generated/HeavyMarkdownEditor" %>
265+
266+
<!-- Before </body> where <%= javascript_pack_tag %> appears -->
267+
<%= javascript_pack_tag "generated/HelloWorld" %>
268+
<%= javascript_pack_tag "generated/HeavyMarkdownEditor" %>
269+
```
270+
271+
This enables optimal bundle splitting where each page only loads the CSS and JavaScript needed for the components actually used on that page.
272+
273+
## Complete Working Example
274+
275+
Here's a step-by-step example showing how to set up file-system-based automated bundle generation from scratch:
276+
277+
### 1. Configure Shakapacker
278+
279+
In `config/shakapacker.yml`:
280+
281+
```yml
282+
default: &default
283+
source_path: app/javascript
284+
source_entry_path: packs
285+
public_root_path: public
286+
public_output_path: packs
287+
nested_entries: true # Required for auto-generation
288+
cache_manifest: false
289+
```
290+
291+
### 2. Configure React on Rails
292+
293+
In `config/initializers/react_on_rails.rb`:
294+
295+
```rb
296+
ReactOnRails.configure do |config|
297+
config.components_subdirectory = "ror_components" # Directory name for auto-registered components
298+
config.auto_load_bundle = true # Enable automatic bundle loading
299+
config.server_bundle_js_file = "server-bundle.js"
300+
end
301+
```
302+
303+
### 3. Directory Structure
304+
305+
Set up your directory structure like this:
306+
307+
```text
308+
app/javascript/
309+
└── src/
310+
├── HelloWorld/
311+
│ ├── HelloWorld.module.css # Component styles
312+
│ └── ror_components/ # Auto-registration directory
313+
│ └── HelloWorld.jsx # React component
314+
└── HeavyMarkdownEditor/
315+
├── HeavyMarkdownEditor.module.css # Component styles
316+
└── ror_components/ # Auto-registration directory
317+
└── HeavyMarkdownEditor.jsx # React component
318+
```
319+
320+
### 4. Component Implementation
321+
322+
`app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx`:
323+
324+
```jsx
325+
import React from 'react';
326+
import styles from '../HelloWorld.module.css';
327+
328+
const HelloWorld = ({ name }) => (
329+
<div className={styles.hello}>
330+
<h1>Hello {name}!</h1>
331+
<p>Welcome to React on Rails with auto-registration!</p>
332+
</div>
333+
);
334+
335+
export default HelloWorld;
336+
```
337+
338+
`app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.jsx`:
339+
340+
```jsx
341+
import React, { useState, useEffect } from 'react';
342+
import styles from '../HeavyMarkdownEditor.module.css';
343+
344+
const HeavyMarkdownEditor = ({ initialContent = '# Hello\n\nStart editing!' }) => {
345+
const [content, setContent] = useState(initialContent);
346+
const [ReactMarkdown, setReactMarkdown] = useState(null);
347+
const [remarkGfm, setRemarkGfm] = useState(null);
348+
349+
// Dynamic imports for SSR compatibility
350+
useEffect(() => {
351+
const loadMarkdown = async () => {
352+
const [{ default: ReactMarkdown }, { default: remarkGfm }] = await Promise.all([
353+
import('react-markdown'),
354+
import('remark-gfm')
355+
]);
356+
setReactMarkdown(() => ReactMarkdown);
357+
setRemarkGfm(() => remarkGfm);
358+
};
359+
loadMarkdown();
360+
}, []);
361+
362+
if (!ReactMarkdown) {
363+
return <div className={styles.loading}>Loading editor...</div>;
364+
}
365+
366+
return (
367+
<div className={styles.editor}>
368+
<div className={styles.input}>
369+
<h3>Markdown Input:</h3>
370+
<textarea
371+
value={content}
372+
onChange={(e) => setContent(e.target.value)}
373+
className={styles.textarea}
374+
/>
375+
</div>
376+
<div className={styles.output}>
377+
<h3>Preview:</h3>
378+
<div className={styles.preview}>
379+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
380+
</div>
381+
</div>
382+
</div>
383+
);
384+
};
385+
386+
export default HeavyMarkdownEditor;
387+
```
388+
389+
### 5. Rails Layout
390+
391+
`app/views/layouts/application.html.erb`:
392+
393+
```erb
394+
<!DOCTYPE html>
395+
<html>
396+
<head>
397+
<title>React on Rails Auto-Registration Demo</title>
398+
<meta name="viewport" content="width=device-width,initial-scale=1">
399+
<%= csrf_meta_tags %>
400+
<%= csp_meta_tag %>
401+
402+
<!-- Empty pack tags - React on Rails injects component CSS/JS here -->
403+
<%= stylesheet_pack_tag %>
404+
<%= javascript_pack_tag %>
405+
</head>
406+
407+
<body>
408+
<%= yield %>
409+
</body>
410+
</html>
411+
```
412+
413+
### 6. Rails Views and Controller
414+
415+
`app/controllers/hello_world_controller.rb`:
416+
417+
```rb
418+
class HelloWorldController < ApplicationController
419+
def index
420+
@hello_world_props = { name: 'Auto-Registration' }
421+
end
422+
423+
def editor
424+
@editor_props = {
425+
initialContent: "# Welcome to the Heavy Editor\n\nThis component demonstrates:\n- Dynamic imports for SSR\n- Bundle splitting\n- Automatic CSS loading"
426+
}
427+
end
428+
end
429+
```
430+
431+
`app/views/hello_world/index.html.erb`:
432+
433+
```erb
434+
<%= react_component("HelloWorld", @hello_world_props, prerender: true) %>
435+
```
436+
437+
`app/views/hello_world/editor.html.erb`:
438+
439+
```erb
440+
<%= react_component("HeavyMarkdownEditor", @editor_props, prerender: true) %>
441+
```
442+
443+
### 7. Generate Bundles
444+
445+
Run the pack generation command:
446+
447+
```bash
448+
bundle exec rake react_on_rails:generate_packs
449+
```
450+
451+
This creates:
452+
- `app/javascript/packs/generated/HelloWorld.js`
453+
- `app/javascript/packs/generated/HeavyMarkdownEditor.js`
454+
455+
### 8. Update .gitignore
456+
457+
```gitignore
458+
# Generated React on Rails packs
459+
**/generated/**
460+
```
461+
462+
### 9. Start the Server
463+
464+
Now when you visit your pages, React on Rails automatically:
465+
- Loads only the CSS and JS needed for components on each page
466+
- Registers components without manual `ReactOnRails.register()` calls
467+
- Enables optimal bundle splitting and caching
468+
469+
**Bundle sizes in this example (measured from browser dev tools):**
470+
- **HelloWorld**: 1.1MB total resources (50KB component-specific code + shared React runtime)
471+
- HelloWorld.js: 10.0 kB
472+
- HelloWorld.css: 2.5 kB
473+
- Shared runtime: ~1.1MB (React, webpack runtime)
474+
- **HeavyMarkdownEditor**: 2.2MB total resources (2.7MB with markdown libraries)
475+
- HeavyMarkdownEditor.js: 26.5 kB
476+
- HeavyMarkdownEditor.css: 5.5 kB
477+
- Markdown libraries: 1,081 kB additional
478+
- Shared runtime: ~1.1MB (React, webpack runtime)
479+
480+
**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)!
481+
482+
#### Performance Screenshots
483+
484+
**HelloWorld (Lightweight Component):**
485+
![HelloWorld Bundle Analysis](../images/bundle-splitting-hello-world.png)
486+
487+
**HeavyMarkdownEditor (Heavy Component):**
488+
![HeavyMarkdownEditor Bundle Analysis](../images/bundle-splitting-heavy-markdown.png)
489+
490+
*Screenshots show browser dev tools network analysis demonstrating the dramatic difference in bundle sizes and load times between the two components.*
491+
222492
### Server Rendering and Client Rendering Components
223493

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

235505
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.
506+
507+
## Troubleshooting
508+
509+
### Common Issues and Solutions
510+
511+
#### 1. "Component not found" errors
512+
513+
**Problem**: `react_component` helper throws "Component not found" error.
514+
515+
**Solutions**:
516+
- Ensure your component is in a `ror_components` directory (or your configured `components_subdirectory`)
517+
- Run `rake react_on_rails:generate_packs` to generate the component bundles
518+
- Check that your component exports a default export: `export default MyComponent;`
519+
- Verify the component name matches the directory structure
520+
521+
#### 2. CSS not loading (FOUC - Flash of Unstyled Content)
522+
523+
**Problem**: Components load but CSS styles are missing or delayed.
524+
525+
**Important**: FOUC (Flash of Unstyled Content) **only occurs with HMR (Hot Module Replacement)**. Static and production modes work perfectly without FOUC.
526+
527+
**Solutions**:
528+
- **Development with HMR** (`./bin/dev`): FOUC is expected behavior due to dynamic CSS injection - **not a bug**
529+
- **Development static** (`./bin/dev static`): No FOUC - CSS is extracted to separate files like production
530+
- **Production** (`./bin/dev prod`): No FOUC - CSS is extracted and optimized
531+
- **Layout**: Verify your layout includes empty `<%= stylesheet_pack_tag %>` placeholder for CSS injection
532+
- **Component imports**: Check that CSS files are properly imported: `import styles from './Component.module.css';`
533+
534+
**Key insight**: Choose your development mode based on your current needs:
535+
- Use HMR for fastest development (accept FOUC)
536+
- Use static mode when testing styling without FOUC
537+
- Use production mode for final testing
538+
539+
#### 3. "document is not defined" errors during SSR
540+
541+
**Problem**: Server-side rendering fails with browser-only API access.
542+
543+
**Solutions**:
544+
- Use dynamic imports for browser-only libraries:
545+
```jsx
546+
useEffect(() => {
547+
const loadLibrary = async () => {
548+
const { default: BrowserLibrary } = await import('browser-library');
549+
setLibrary(() => BrowserLibrary);
550+
};
551+
loadLibrary();
552+
}, []);
553+
```
554+
- Provide fallback/skeleton components during loading
555+
- Consider client-only rendering: use `ComponentName.client.jsx` files only
556+
557+
#### 4. Bundles not being generated
558+
559+
**Problem**: Running `rake react_on_rails:generate_packs` doesn't create files.
560+
561+
**Solutions**:
562+
- Verify `nested_entries: true` in `shakapacker.yml`
563+
- Check that `components_subdirectory` is correctly configured
564+
- Ensure components are in the right directory structure: `src/ComponentName/ror_components/ComponentName.jsx`
565+
- Make sure you're using the correct source path in Shakapacker config
566+
567+
#### 5. Manual pack tags not working after switching to auto-loading
568+
569+
**Problem**: Manually specified `javascript_pack_tag` or `stylesheet_pack_tag` break.
570+
571+
**Solutions**:
572+
- Remove specific pack names from manual pack tags: use `<%= javascript_pack_tag %>` instead of `<%= javascript_pack_tag 'specific-bundle' %>`
573+
- Remove manual `append_javascript_pack_tag` calls - `react_component` with `auto_load_bundle: true` handles this automatically
574+
- Delete any client bundle entry files (e.g., `client-bundle.js`) that manually register components
575+
576+
#### 6. Bundle size issues
577+
578+
**Problem**: Large bundles loading when not needed.
579+
580+
**Solutions**:
581+
- Use component-level bundle splitting - each page loads only needed components
582+
- Implement dynamic imports for heavy dependencies
583+
- Check bundle analysis with `RAILS_ENV=production NODE_ENV=production bundle exec rails assets:precompile` and examine generated bundle sizes
584+
- Consider code splitting within heavy components
585+
586+
#### 7. Development vs Production differences
587+
588+
**Problem**: Works in development but fails in production.
589+
590+
**Solutions**:
591+
- **CSS**: Production extracts CSS to separate files, development might inline it
592+
- **Source maps**: Check if source maps are causing issues in production
593+
- **Minification**: Some code might break during minification - check console for errors
594+
- **Environment**: Use `bin/dev prod` to test production-like assets locally
595+
596+
#### 8. Installation order issues
597+
598+
**Problem**: React on Rails installation fails or behaves unexpectedly.
599+
600+
**Solutions**:
601+
- **Correct order**: Always install Shakapacker BEFORE React on Rails
602+
```bash
603+
bundle add shakapacker
604+
rails shakapacker:install
605+
bundle add react_on_rails
606+
rails generate react_on_rails:install
607+
```
608+
- If you installed in wrong order, uninstall and reinstall in correct sequence
609+
610+
### Debug Mode
611+
612+
To debug auto-loading behavior, temporarily add logging to see what bundles are being loaded:
613+
614+
```erb
615+
<!-- Temporarily add this to your layout to see what gets loaded -->
616+
<%= debug(content_for(:javascript_pack_tags)) %>
617+
<%= debug(content_for(:stylesheet_pack_tags)) %>
618+
```
619+
620+
This helps verify that components are correctly appending their bundles.
403 KB
Loading

0 commit comments

Comments
 (0)