react-manifest-rails
Generate per-controller Sprockets manifests for React code in Rails.
Instead of one monolithic application.js, each controller gets a lean ux_<controller>.js bundle that only loads what it needs. Shared components, hooks, and libraries are automatically bundled into a single ux_shared.js included on every page.
Quick Start
1. Add to Gemfile
gem "react-manifest-rails"
group :development do
gem "listen", "~> 3.0" # optional — enables auto-regeneration on file change
end
bundle install
2. Add initializer
# config/initializers/react_manifest.rb
ReactManifest.configure do |config|
# All defaults shown — only override what you need to change.
config.ux_root = "app/assets/javascripts/ux"
config.app_dir = "app"
config.output_dir = "app/assets/javascripts"
end
3. Run setup
bundle exec rails react_manifest:setup
This one command:
- Patches
application.jsto remove globally-required React files (they'll be per-controller instead) - Patches
app/assets/config/manifest.jsto add thelink_treedirective Sprockets needs to compile the bundles - Patches your layout(s) to insert
react_bundle_tag - Generates the initial
ux_*.jsmanifests
Preview changes without writing anything:
DRY_RUN=1 bundle exec rails react_manifest:setup
4. Organise your React files
app/assets/javascripts/ux/
app/
users/ → compiled into ux_users.js
users_index.jsx
dashboard/ → compiled into ux_dashboard.js
dashboard.jsx
components/ → compiled into ux_shared.js (included on every page)
nav.jsx
button.jsx
hooks/ → also goes into ux_shared.js
use_modal.js
lib/ → also goes into ux_shared.js
helpers.js
5. Start the server
bundle exec rails s
In development:
- Missing
ux_*.jsfiles are generated automatically on boot. - If
listenis installed, saving any file underux/regenerates affected manifests instantly. - Without
listen, runbundle exec rails react_manifest:generateafter adding files.
How Bundle Inclusion Works
Generation is directory-based — deterministic and conservative by design.
ux_shared.js: every file from directories outsideux/app/(i.e.components/,hooks/,lib/, etc.)ux_<controller>.js:ux_shared+ every file underux/app/<controller>/
Namespace fallback for nested controllers: admin/reports/summary tries ux_admin_reports_summary, then ux_admin_reports, then ux_admin, then ux_summary. The most specific match wins.
The gem's scanner uses regex to detect which shared symbols are referenced in each controller directory (for the react_manifest:analyze report). Generation itself stays directory-based to avoid brittle runtime misses from dynamic component references.
What Gets Generated
app/assets/javascripts/ux_manifests/ ← generated; do not edit
ux_shared.js
ux_users.js
ux_dashboard.js
...
Files carry an AUTO-GENERATED header. Any file without it is never overwritten — you can pin a manifest by removing the header.
Writes are atomic (temp file + rename) and idempotent (SHA-256 comparison skips unchanged files).
Asset Compilation & Minification
The generated files are standard Sprockets manifests — //= require directives only. Sprockets processes them identically to application.js:
- Development: concatenated and served from memory.
- Production (
assets:precompile): concatenated, minified (with whatever JS compressor your app uses —uglifier/mini_racer,terser, libv8, etc.), digested, and gzipped.
The gem hooks into assets:precompile as a prerequisite, so manifest generation always runs before Sprockets begins compiling.
Configuration
ReactManifest.configure do |config|
config.ux_root = "app/assets/javascripts/ux"
config.app_dir = "app" # subdirectory of ux_root containing per-controller dirs
config.output_dir = "app/assets/javascripts"
config.manifest_subdir = "ux_manifests" # subdirectory of output_dir for generated files
config.shared_bundle = "ux_shared"
config.extensions = %w[js jsx] # add ts tsx for TypeScript
config.always_include = [] # extra shared files always added to every bundle
config.ignore = [] # controller dir names to skip entirely
config.exclude_paths = ["react", "react_dev", "vendor"] # path segments to exclude
config.size_threshold_kb = 500 # warn if a bundle exceeds this
config.dry_run = false # never write; only print what would change
config.verbose = false # extra diagnostic detail
config.stdout_logging = true # print status lines to terminal
end
Key option notes
ignore: skips entire controller dirs underux/app/.ignore = ["admin"]excludesux/app/admin/.exclude_paths: excludes files whose path contains any listed segment. Not based onapplication.js.dry_run: also honoured byDRY_RUN=1environment variable at runtime.extensions: addtsandtsxto enable TypeScript source detection.
Commands
# First-time setup (patches application.js, manifest.js, layouts; generates manifests)
bundle exec rails react_manifest:setup
# Regenerate all manifests
bundle exec rails react_manifest:generate
# Preview any command without writing
DRY_RUN=1 bundle exec rails react_manifest:setup
DRY_RUN=1 bundle exec rails react_manifest:generate
# Analyse which shared symbols are used per controller
bundle exec rails react_manifest:analyze
# Print bundle size report
bundle exec rails react_manifest:report
# Watch for changes in foreground (debugging only — dev server already does this)
bundle exec rails react_manifest:watch
# Remove all generated manifests
bundle exec rails react_manifest:clean
Troubleshooting
AssetNotPrecompiledError for ux_*.js
Sprockets 4 requires an explicit link_tree directive to compile files from non-standard paths. Run setup (or manually add the directive):
bundle exec rails react_manifest:setup
This adds //= link_tree ../javascripts/ux_manifests .js to app/assets/config/manifest.js.
react_manifest tasks not found
bundle exec rails -T | grep react_manifest
If nothing appears:
- Confirm the gem is in
Gemfileand installed (bundle show react-manifest-rails). - Ensure it is not loaded with
require: false. - Restart Spring:
bin/spring stop.
Server starts but no bundles are served
Check in order:
app/assets/javascripts/ux/exists.- Controller files are under
ux/app/<controller>/. - Your layout includes
<%= react_bundle_tag %>. - Run
bundle exec rails react_manifest:generateand confirm files appear inux_manifests/. - Confirm
app/assets/config/manifest.jscontains thelink_treedirective (run setup again if missing).
Auto-watch not running in development
- Add
listento the development group in your Gemfile andbundle install. - Restart the Rails server.
- Without
listen, runreact_manifest:generatemanually after making changes.
Compatibility
- Ruby: 3.2+
- Rails: 7.x – 8.x
- Asset pipeline: Sprockets 3 and 4
- JS compressors: uglifier / mini_racer, terser, libv8 / therubyracer — all work transparently
License
MIT