Webpacker

Learn about webpacker in Ruby on Rails development.

Beginner 1 min read

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 (
setTitle(e.target.value)} required />