Webpacker
Learn how to use Webpacker to manage modern JavaScript and CSS with webpack in Rails applications (Rails 6 and early Rails 7).
Webpacker Overview
# Install Webpacker (Rails 6)
rails webpacker:install
# Install specific framework integrations
rails webpacker:install:react
rails webpacker:install:vue
rails webpacker:install:angular
rails webpacker:install:elm
rails webpacker:install:stimulus
# Gemfile
gem 'webpacker', '~> 5.4'
# Or for Rails 7 (if still using Webpacker)
gem 'webpacker', '~> 6.0'
Webpacker vs Asset Pipeline
- Modern JS: ES6+, TypeScript, JSX support out of the box
- Module bundling: Webpack's advanced code splitting
- Hot reloading: Faster development with webpack-dev-server
- NPM ecosystem: Access to the entire JavaScript ecosystem
- Framework integration: Built-in React, Vue, Angular support
Deprecation Notice: Webpacker was retired as the default in Rails 7. Rails 7+ uses import maps by default, though Webpacker can still be added manually.
Webpacker Directory Structure
# Webpacker directory structure
app/
└── javascript/
├── packs/ # Entry points
│ ├── application.js # Main entry point
│ ├── admin.js # Admin-specific entry
│ └── hello_react.jsx # React entry point
├── channels/ # Action Cable channels
├── controllers/ # Stimulus controllers
├── components/ # Reusable components
│ ├── Header.jsx
│ └── Footer.vue
├── stylesheets/ # CSS/SCSS files
│ ├── application.scss
│ └── components/
└── images/ # Images processed by Webpack
config/
├── webpacker.yml # Webpacker configuration
└── webpack/
├── development.js
├── production.js
├── test.js
└── environment.js # Base webpack config
node_modules/ # NPM packages
package.json # NPM dependencies
yarn.lock # Yarn lockfile
# config/webpacker.yml
default: &default
source_path: app/javascript
source_entry_path: packs
public_root_path: public
public_output_path: packs
cache_path: tmp/cache/webpacker
webpack_compile_output: true
# Additional paths webpack should lookup modules
resolved_paths:
- app/assets
# Reload manifest.json on all requests so we reload latest compiled packs
cache_manifest: false
# Extract and emit a css file
extract_css: false
development:
<<: *default
compile: true
# Reference: https://webpack.js.org/configuration/dev-server/
dev_server:
host: localhost
port: 3035
public: localhost:3035
hmr: false
# Inline should be set to true if using HMR
inline: true
overlay: true
compress: true
disable_host_check: true
use_local_ip: false
quiet: false
pretty: false
headers:
'Access-Control-Allow-Origin': '*'
watch_options:
ignored: '**/node_modules/**'
production:
<<: *default
# Production depends on precompilation of packs prior to booting for performance.
compile: false
# Extract and emit a css file
extract_css: true
# Cache manifest.json for performance
cache_manifest: true
JavaScript with Webpacker
// app/javascript/packs/application.js
import Rails from "@rails/ujs"
import Turbo from "@hotwired/turbo-rails"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
// Import stylesheets
import "../stylesheets/application.scss"
// Import custom JavaScript
import "../components/navbar"
import "../components/modal"
Rails.start()
Turbo.start()
ActiveStorage.start()
// Make functions available globally
window.Rails = Rails
console.log('Webpacker application loaded')
// app/javascript/components/navbar.js
class Navbar {
constructor() {
this.init()
}
init() {
this.bindEvents()
this.setupMobileToggle()
}
bindEvents() {
document.addEventListener('DOMContentLoaded', () => {
this.setupDropdowns()
})
// Turbo events
document.addEventListener('turbo:load', () => {
this.setupDropdowns()
})
}
setupMobileToggle() {
const toggle = document.querySelector('.navbar-toggle')
const menu = document.querySelector('.navbar-menu')
if (toggle && menu) {
toggle.addEventListener('click', () => {
menu.classList.toggle('active')
})
}
}
setupDropdowns() {
const dropdowns = document.querySelectorAll('.dropdown')
dropdowns.forEach(dropdown => {
const trigger = dropdown.querySelector('.dropdown-trigger')
const menu = dropdown.querySelector('.dropdown-menu')
if (trigger && menu) {
trigger.addEventListener('click', (e) => {
e.preventDefault()
menu.classList.toggle('active')
})
}
})
}
}
// Initialize navbar
new Navbar()
export default Navbar
// app/javascript/components/modal.js
export default class Modal {
constructor(element) {
this.modal = element
this.overlay = this.modal.querySelector('.modal-overlay')
this.closeBtn = this.modal.querySelector('.modal-close')
this.bindEvents()
}
bindEvents() {
if (this.closeBtn) {
this.closeBtn.addEventListener('click', () => this.close())
}
if (this.overlay) {
this.overlay.addEventListener('click', () => this.close())
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.close()
})
}
open() {
this.modal.classList.add('active')
document.body.classList.add('modal-open')
}
close() {
this.modal.classList.remove('active')
document.body.classList.remove('modal-open')
}
static init() {
const modals = document.querySelectorAll('.modal')
modals.forEach(modal => {
new Modal(modal)
})
// Setup modal triggers
const triggers = document.querySelectorAll('[data-modal]')
triggers.forEach(trigger => {
trigger.addEventListener('click', (e) => {
e.preventDefault()
const modalId = trigger.dataset.modal
const modal = document.getElementById(modalId)
if (modal && modal.modalInstance) {
modal.modalInstance.open()
}
})
})
}
}
React Integration
# Install React with Webpacker
rails webpacker:install:react
# This adds React dependencies to package.json
yarn add react react-dom
yarn add --dev @babel/preset-react
// app/javascript/packs/hello_react.jsx
import React from 'react'
import ReactDOM from 'react-dom'
// Import components
import App from '../components/App'
import Dashboard from '../components/Dashboard'
// Component registry
const components = {
App,
Dashboard
}
// Auto-initialize React components
document.addEventListener('DOMContentLoaded', () => {
const reactElements = document.querySelectorAll('[data-react-component]')
reactElements.forEach(element => {
const componentName = element.getAttribute('data-react-component')
const props = JSON.parse(element.getAttribute('data-react-props') || '{}')
const Component = components[componentName]
if (Component) {
ReactDOM.render( , element)
} else {
console.error(`React component '${componentName}' not found`)
}
})
})
// app/javascript/components/App.jsx
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const App = ({ initialMessage = 'Hello from React!' }) => {
const [message, setMessage] = useState(initialMessage)
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchPosts()
}, [])
const fetchPosts = async () => {
try {
const response = await axios.get('/api/posts.json')
setPosts(response.data)
setLoading(false)
} catch (error) {
console.error('Error fetching posts:', error)
setLoading(false)
}
}
const handleSubmit = async (formData) => {
try {
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
await axios.post('/api/posts', formData, {
headers: {
'X-CSRF-Token': token,
'Content-Type': 'application/json'
}
})
fetchPosts() // Refresh list
} catch (error) {
console.error('Error creating post:', error)
}
}
if (loading) {
return Loading...
}
return (
{message}
{posts.map(post => (
{post.title}
{post.content}
))}
)
}
const PostForm = ({ onSubmit }) => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
onSubmit({ title, content })
setTitle('')
setContent('')
}
return (
)
}
export default App
<!-- Using React components in ERB templates -->
<%= javascript_pack_tag 'hello_react' %>
<!-- React component mounting point -->
<div data-react-component="App"
data-react-props="<%= { initialMessage: 'Welcome to our app!' }.to_json %>">
</div>
<!-- Dashboard component with user data -->
<div data-react-component="Dashboard"
data-react-props="<%= { user: current_user, posts: @posts }.to_json %>">
</div>
CSS & Styling with Webpacker
// app/javascript/stylesheets/application.scss
// Import external libraries
@import "~bootstrap/scss/bootstrap";
@import "~@fortawesome/fontawesome-free/css/all.css";
// Import custom styles
@import "./base/variables";
@import "./base/typography";
@import "./components/buttons";
@import "./components/forms";
@import "./components/modal";
// Component-specific styles
@import "./pages/home";
@import "./pages/dashboard";
// app/javascript/stylesheets/base/_variables.scss
// Color palette
$primary: #007bff;
$secondary: #6c757d;
$success: #28a745;
$warning: #ffc107;
$danger: #dc3545;
// Typography
$font-family-sans-serif: "Helvetica Neue", Arial, sans-serif;
$font-size-base: 1rem;
$line-height-base: 1.5;
// Spacing
$spacer: 1rem;
$spacers: (
0: 0,
1: $spacer * 0.25,
2: $spacer * 0.5,
3: $spacer,
4: $spacer * 1.5,
5: $spacer * 3
);
// Breakpoints
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px
);
// app/javascript/stylesheets/components/_modal.scss
.modal {
display: none;
position: fixed;
z-index: 1050;
top: 0;
left: 0;
width: 100%;
height: 100%;
&.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background: white;
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
z-index: 1051;
.modal-header {
padding: 1rem;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
h5 {
margin: 0;
}
}
.modal-body {
padding: 1rem;
}
.modal-footer {
padding: 1rem;
border-top: 1px solid #e9ecef;
text-align: right;
.btn + .btn {
margin-left: 0.5rem;
}
}
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #adb5bd;
&:hover {
color: #6c757d;
}
}
}
body.modal-open {
overflow: hidden;
}
Development Server & Hot Reloading
# Start webpack dev server
./bin/webpack-dev-server
# Or using foreman/overmind
# Procfile.dev
web: bundle exec rails server
webpacker: ./bin/webpack-dev-server
# Start both servers
overmind start -f Procfile.dev
# or
foreman start -f Procfile.dev
// config/webpack/development.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
const environment = require('./environment')
// Enable HMR for development
environment.config.merge({
devServer: {
hot: true,
liveReload: true,
client: {
overlay: {
errors: true,
warnings: false,
},
},
},
})
module.exports = environment.toWebpackConfig()
# config/environments/development.rb
Rails.application.configure do
# Enable/disable caching for development
config.cache_classes = false
config.eager_load = false
# Configure Webpacker dev server
config.webpacker.check_yarn_integrity = false
config.webpacker.dev_server.enabled = true
config.webpacker.dev_server.host = 'localhost'
config.webpacker.dev_server.port = 3035
# Hot module replacement
config.webpacker.dev_server.hmr = false
config.webpacker.dev_server.inline = true
config.webpacker.dev_server.overlay = true
end
Production Deployment
# Precompile Webpacker assets
rails assets:precompile
# Or Webpacker-specific command
rails webpacker:compile
# Clean old compiled assets
rails webpacker:clean
# Verify Webpacker installation
rails webpacker:verify_install
# config/environments/production.rb
Rails.application.configure do
# Webpacker production configuration
config.webpacker.check_yarn_integrity = false
config.webpacker.compile = false
# Extract CSS into separate files
config.webpacker.extract_css = true
# Enable asset host for CDN
config.asset_host = 'https://cdn.myapp.com'
# Gzip compression
config.middleware.use Rack::Deflater
end
// config/webpack/production.js
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const environment = require('./environment')
// Production optimizations
environment.config.merge({
optimization: {
moduleIds: 'deterministic',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
// Source maps for production debugging
devtool: 'source-map',
})
module.exports = environment.toWebpackConfig()