IslandJS Rails supports the development of React (or other JS library) islands in Rails apps by synchronizing package.json defined dependencies with UMD libraries served in public/islands/vendor.
Write Turbo compatible JSX in app/javascript/islands/components/ and render it with a react_component helper in ERB templates (including Turbo Stream partials) — Vue and other framework support can be added with a bit of work.
<%= react_component('DashboardApp', { userId: current_user.id }) %>
<%= react_component('DashboardApp', { userId: current_user.id }) do %>
<div class="loading-skeleton">Loading dashboard...</div>
<% end %>"><!-- In any view --><%=react_component('DashboardApp',{userId: current_user.id})%><!-- With placeholder (v0.2.0+) to prevent layout shift --><%=react_component('DashboardApp',{userId: current_user.id})do%><divclass="loading-skeleton">Loading dashboard...</div><%end%>
yarn build # you may remove any stale islandjs bundles before commit
💡 Turbo Cache Compatible: React components automatically persist state across Turbo navigation! See Turbo Cache Integration for details.
Write Modern JSX (with Turbo Cache Support)
Every React component should be written to accept a single containerId prop and rendered using the react_component view helper, which accepts a JSON object of props.
The props data passed into react_component is automatically available via useTurboProps and can be optionally cached using useTurboCache for persistence across Turbo navigation.
// jsx/components/DashboardApp.jsximportReact,{useState,useEffect}from'react';import{useTurboProps,useTurboCache}from'../utils/turbo.js';functionDashboardApp({ containerId }){// Read initial state from data-initial-state attributeconstinitialProps=useTurboProps(containerId);const[userId]=useState(initialProps.userId);const[welcomeCount,setWelcomeCount]=useState(initialProps.welcomeCount||0);// Setup turbo cache persistence for state across navigationuseEffect(()=>{constcleanup=useTurboCache(containerId,{ userId, welcomeCount },true);returncleanup;},[containerId,userId,welcomeCount]);return(<div><h2>Welcome user {userId}!</h2><p>You've visited this dashboard {welcomeCount} times</p><buttononClick={()=>setWelcomeCount(prev=>prev+1)}>
Visit Again
</button></div>);}exportdefaultDashboardApp;
Do not pass sensitive data to the client-side via props. Pass it any other (secure) way — props are encoded in the HTML and are visible to the client and any other scripts.
IslandJS Rails aligns perfectly with Rails 8's philosophy of simplicity and convention over configuration:
Skip modern JS: Miss out on React and popular npm packages
Important Note: IslandJS Rails works with packages that ship UMD builds. Many popular packages have UMD builds, but some modern packages do not — React 19+ removed UMD builds entirely. Future versions of IslandJS Rails will support local UMD generation for some packages (such as React 19+).
Webpack Externals - Updates webpack config to prevent duplicate bundling while allowing development in jsx or other formats
Placeholder Support - Eliminate layout shift with automatic placeholder management ⚡ New in v0.2.0
Flexible Architecture - Compose and namespace libraries as needed
# Initialize IslandJS Rails in your project
rails islandjs:init
# Install packages (adds to package.json + saves to vendor directory)
rails "islandjs:install[react]"
rails "islandjs:install[react,18.3.1]"# With specific version
rails "islandjs:install[lodash]"# Update packages (updates package.json + refreshes vendor files)
rails "islandjs:update[react]"
rails "islandjs:update[react,18.3.1]"# To specific version# Remove packages (removes from package.json + deletes vendor files)
rails "islandjs:remove[react]"
rails "islandjs:remove[lodash]"# Clean all UMD files (removes ALL vendor files)
rails islandjs:clean
# Show configuration
rails islandjs:config
🗂️ Vendor System Management
IslandJS Rails includes additional tasks for managing the vendor file system:
# Rebuild the combined vendor bundle (when using :external_combined mode)
rails islandjs:vendor:rebuild
# Show vendor system status and file sizes
rails islandjs:vendor:status
Vendor System Modes:
:external_split (default): Each library served as separate file from public/islands/vendor/
:external_combined: All libraries concatenated into single bundle with cache-busting hash
Benefits of Vendor System:
🚀 Better Performance: Browser caching, parallel downloads, no Base64 bloat
📦 Scalable: File size doesn't affect HTML parsing or memory usage
🔧 Maintainable: Clear separation between vendor libraries and application code
🌐 CDN Ready: Vendor files can be easily moved to CDN for global distribution (serving from CDN will be configurable granularly in future versions — where possible)
🛠️ Development & Production Commands
For development and building your JavaScript:
# Development - watch for changes and rebuild automatically
yarn watch
# Or with npm: npm run watch# Production - build optimized bundle for deployment
yarn build
# Or with npm: npm run build# Install dependencies (after adding packages via islandjs:install)
yarn install
# Or with npm: npm install
Development Workflow:
Run yarn watch (or npm run watch) in one terminal
Edit your components in app/javascript/islands/components/
Changes are automatically compiled to public/
Production Deployment:
Run yarn build (or npm run build) to create optimized bundle
Commit the built assets: git add public/islands_* && git add public/islands/*
Deploy with confidence - assets are prebuilt
📦 Working with Scoped Packages
What are Scoped Packages?
Scoped packages are npm packages that belong to a namespace, prefixed with @. Examples include:
@solana/web3.js
When installing scoped packages, you must include the full package name with the @ symbol:
The @ symbol is handled automatically by Rails task syntax when using double quotes. No additional escaping is needed:
# ✅ Works perfectly
rails "islandjs:install[@solana/web3.js]"# ✅ Also works (with version)
rails "islandjs:install[@solana/web3.js,1.98.4]"# ⚠️ May not work in some shells without quotes
rails islandjs:install[@solana/web3.js] # Avoid this
IslandJS Rails automatically converts scoped package names to valid JavaScript global names:
You can override the automatic global name detection for scoped packages:
Solana Web3.js is automatically detected with the built-in global name mapping solanaWeb3.
Once installed, scoped packages work exactly like regular packages:
// jsx/components/SolanaComponent.jsximportReactfrom'react';functionSolanaComponent(){// solanaWeb3 is automatically available as a global variable on the window objectconstconnection=newwindow.solanaWeb3.Connection('https://api.devnet.solana.com');return(<div><h2>Solana Integration</h2><p>Connected to: {connection.rpcEndpoint}</p></div>);}exportdefaultSolanaComponent;
IslandJS Rails automatically configures webpack externals for scoped packages:
// webpack.config.js (auto-generated)module.exports={externals: {// IslandJS Rails managed externals - do not edit manually"@solana/web3.js": "solanaWeb3","react": "React","react-dom": "ReactDOM"},// ... rest of config};
Troubleshooting Scoped Packages
Issue: Package not found
# Check the exact package name on npm
npm view @solana/web3.js
# Ensure you're using the full name
rails "islandjs:install[@solana/web3.js]"# ✅ Correct
rails "islandjs:install[@solana/web3]"# ❌ Wrong
Issue: UMD not available
# Some scoped packages don't ship UMD builds# Check package documentation or try alternatives# Future IslandJS Rails versions will support local UMD generation
Command
What it does
Example
install
Adds package via yarn + downloads UMD + saves to vendor
rails islandjs:install[react]
update
Updates package version + refreshes UMD
rails islandjs:update[react,18.3.1]
remove
Removes package via yarn + deletes vendor files
rails islandjs:remove[react]
clean
Removes ALL vendor files (destructive!)
rails islandjs:clean
# config/initializers/islandjs.rbIslandjsRails.configuredo |config|
# Directory for ERB partials (default: app/views/shared/islands)config.partials_dir=Rails.root.join('app/views/shared/islands')# Webpack configuration pathconfig.webpack_config_path=Rails.root.join('webpack.config.js')# Vendor file delivery mode (default: :external_split)config.vendor_script_mode=:external_split# One file per library# config.vendor_script_mode = :external_combined # Single combined bundle# Vendor files directory (default: public/islands/vendor)config.vendor_dir=Rails.root.join('public/islands/vendor')# Combined bundle filename base (default: 'islands-vendor')config.combined_basename='islands-vendor'# Library loading order for combined bundlesconfig.vendor_order=['react','react-dom','lodash']end
Single helper that includes all UMD vendor scripts and your webpack bundle.
This automatically loads:
All UMD libraries from vendor files (either split or combined mode)
Your webpack bundle
Debug information in development
react_component(name, props, options, &block)
Renders a React component with Turbo-compatible lifecycle and optional placeholder support.
namespace: JavaScript namespace for component access (default: window.islandjsRails)
tag: HTML tag for container (default: div)
class: CSS class for container
placeholder_class: CSS class for placeholder content
placeholder_style: Inline styles for placeholder content
⚡ New in v0.2.0 - Prevent layout shift when React components mount!
The react_component helper now supports placeholder content that displays while your React component loads, eliminating the "jumpy" effect common in dynamic content updates via Turbo Streams.
When React components mount (especially via Turbo Stream updates), there's often a brief moment where content height changes, causing layout shift:
<%= react_component("Reactions", { postId: post.id }) %>
'><!-- Before: Content jumps when component mounts --><%=react_component("Reactions",{postId: post.id})%><!-- Page content shifts down when reactions component renders -->
Solution: Three Placeholder Patterns
1. ERB Block Placeholder (Most Flexible)
<%= react_component("Reactions", { postId: post.id }) do %><divclass="reactions-skeleton"><divclass="skeleton-button">👍</div><divclass="skeleton-button">❤️</div><divclass="skeleton-button">🚀</div><divclass="skeleton-count">Loading...</div></div><%end%>
importReact,{useState,useEffect}from'react';import{useTurboProps,useTurboCache}from'../utils/turbo.js';constHelloWorld=({ containerId })=>{// Read initial state from data-initial-state attributeconstinitialProps=useTurboProps(containerId);const[count,setCount]=useState(initialProps.count||0);const[message,setMessage]=useState(initialProps.message||"Hello!");// ensures persists state across Turbo navigationuseEffect(()=>{constcleanup=useTurboCache(containerId,{ count, message },true);returncleanup;},[containerId,count,message]);return(<div><p>{message}</p><buttononClick={()=>setCount(count+1)}>
Clicked {count} times
</button></div>);};
<%= react_component('HelloWorld', {
message: 'Hello from Rails!',
count: 5
}) %>"><!-- In any Rails view --><%=react_component('HelloWorld',{message: 'Hello from Rails!',count: 5})%>
IslandJS Rails provides utility functions for Turbo compatibility:
// Get initial state from container's data attributeconstinitialProps=useTurboProps(containerId);// Set up automatic state persistenceconstcleanup=useTurboCache(containerId,currentState,autoRestore);// Manually persist state (if needed)persistState(containerId,stateObject);
🔄 Seamless Navigation: State survives Turbo page transitions
⚡ Zero Setup: Works automatically with react_component helper
🎯 Rails-Native: Designed specifically for Rails + Turbo workflows
🏝️ Island Architecture: Each component manages its own state independently
IslandJS Rails includes built-in global name mappings for popular libraries:
react → React
react-dom → ReactDOM
lodash → _
@solana/web3.js → solanaWeb3
And more common libraries
For other packages, kebab-case names are automatically converted to camelCase.
// Create your own namespace (or use the default window.islandjsRails)window.islandjsRails={React: window.React,UI: window.MaterialUI,Utils: window._,Charts: window.Chart};// Use in componentsconst{ React,UI, Utils }=window.islandjsRails;
IslandJS Rails automatically updates your webpack externals:
IslandjsRails.configuredo |config|
# Directory for ERB partials (default: app/views/shared/islands)config.partials_dir=Rails.root.join('app/views/shared/islands')# Path to webpack config (default: webpack.config.js)config.webpack_config_path=Rails.root.join('webpack.config.js')# Path to package.json (default: package.json)config.package_json_path=Rails.root.join('package.json')# Vendor file delivery mode (default: :external_split)config.vendor_script_mode=:external_split# One file per library# config.vendor_script_mode = :external_combined # Single combined bundle# Vendor files directory (default: public/islands/vendor)config.vendor_dir=Rails.root.join('public/islands/vendor')# Combined bundle filename base (default: 'islands-vendor')config.combined_basename='islands-vendor'# Library loading order for combined bundlesconfig.vendor_order=['react','react-dom','lodash']# Built-in global name mappings are automatically applied# No custom configuration needed for common librariesend
Package not found on CDN:
# Some packages don't publish UMD builds# Check unpkg.com/package-name/ for available files# Consider using a different package or requesting UMD support
Global name conflicts:
IslandJS Rails includes built-in mappings for common libraries. For packages with unusual global names, check the library's documentation or browser console to find the correct global variable name.
Webpack externals not updating:
# Sync to update externals
rails islandjs:sync
# Or clean and reinstall
rails islandjs:clean
rails islandjs:install[react]
Fork the repository
Create your feature branch (git checkout -b feature/amazing-feature)
Run the tests (bundle exec rspec)
Commit your changes (git commit -am 'Add amazing feature')
Push to the branch (git push origin feature/amazing-feature)
Open a Pull Request
MIT License - see LICENSE file for details.
cd lib/islandjs_rails
bundle install
bundle exec rspec
# View coverage in terminal
bundle exec rspec
# Open coverage report in browser
open coverage/index.html
Planned features for future releases:
Server-Side Rendering (SSR): Pre-render React components on the server
Component Caching: Intelligent caching of rendered components
Hot Reloading: Development mode hot reloading for React components
TypeScript Support: First-class TypeScript support for UMD packages
Local UMD Generation: Generate UMD builds for packages that don't ship them
Multi-framework Support: Vue, Svelte, and other frameworks
Rails 8 Integration Benefits
🚀 Perfect for Rails 8 Philosophy
Convention over Configuration: Install React in one command
The Rails Way: Simple, opinionated, productive
Modern Without Complexity: React islands, not SPAs
Instant Builds: No bundling external libraries
Small Bundles: Only your app code gets bundled
Fast Deploys: CDN libraries cache globally
Zero Webpack Expertise: Rails developers stay in Rails
Turbo Compatible: Seamless navigation and caching
Progressive Enhancement: Start with Hotwire, add React islands