Mastering Packs in Webpacker

In the previous article, we saw what Webpacker is and how Rails 6 has integrated it. In this article, we will understand how to use the packs.

A new Rails 6 app creates following files under app/javascript, the new destination for writing our JavaScript code.

Projects/scratch/better_hn  master ✗ 2.6.3                                                        ◒
▶ tree app/javascript
app/javascript
├── channels
│   ├── consumer.js
│   └── index.js
└── packs
    └── application.js

2 directories, 3 files

The packs directory contains the entry points for webpack to start the compiling process. The content of this file in a new Rails 6 app are as follows.

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
The entry points are analogues to the app/assets/application.js file generated by asset pipeline.

The require statement here is not same as the require directive from the asset pipeline. This require statement can require the NPM packages as well as local modules from our code. For eg. in this case, the first three lines above require three NPM packages - Rails UJS, Turbolinks and Active Storage whereas the last line requires app/javascript/channels/index.js.

Webpack has a convention for looking for index.js file under the directory name we are trying to require.

Main difference between the way requiring code between Webpacker and asset pipeline is that we don't use the directives in Webpacker packs. We just directly call require or import with the package names or directory names.

// app/javascript/packs/application.js

import React from 'react'
import ReactDOM from 'react-dom'

The pack file can also have actual JavaScript code related to the application but it is good practice to keep the pack files clean only with minimal code importing the modules and packages and keeping the actual application related code outside of app/javascript/packs.

Keep pack files minimal and import the actual code in it.

So for a React project, we will just add our base component to the DOM in the pack file and manage rest of the components under app/javascript outside of the pack directory.

// app/javascript/packs/application.js
import ReactDOM from 'react-dom'
import HelloWorld from '../components/hello'

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <HelloWorld name="Webpack" />,
    document.body.appendChild(document.createElement('div')),
  )
})
// app/javascript/components/hello.js

import React from 'react'
import PropTypes from 'prop-types'

const HelloWorld = props => (
  <div>Hello {props.name}!</div>
)

Hello.defaultProps = {
  name: 'David'
}

Hello.propTypes = {
  name: PropTypes.string
}

export default HelloWorld

Webpacker is pretty liberal in terms of how one should organize their JavaScript code. The only rule is that packs directory is special and is treated as entry points by Webpacker. Rest is up to you. I prefer adding a components directory for all the React components. My application has application pack as the default pack and admin pack which has code only related to the admin users.

In the Sprockets world, we have to add every custom file that we want Rails to precompile, in the asset precompile list.

Rails.application.config.assets.precompile += %w[admin.js]

But because the whole packs directory is considered as entry points by Webpacker, we don't have to add any of the custom packs to the precompile list. It just works!

In general, I will say that we should treat the app/javascript directory as an application within application itself and organize the code accordingly. For eg. in one of my application, the directory structure of the app/javascript is as follows.

app/javascript
├── admin
├── channels
├── login
└── packs
    ├── admin.js
    ├── application.js
    └── login.js

When we compile this JavaScript, the output looks like following.

▶ ./bin/webpack-dev-server
ℹ 「wds」: Project is running at http://localhost:3035/
ℹ 「wds」: webpack output is served from /packs/
ℹ 「wds」: Content not from webpack is served from /Users/prathamesh/Projects/scratch/better_hn/public/packs
ℹ 「wds」: 404s will fallback to /index.html
ℹ 「wdm」: Hash: 5387bbdba96d7150c792
Version: webpack 4.39.2
Time: 2753ms
Built at: 09/24/2019 12:23:20 AM
                                     Asset       Size       Chunks             Chunk Names
          js/admin-67dd60bc5c69e9e06cc3.js    385 KiB        admin  [emitted]  admin
      js/admin-67dd60bc5c69e9e06cc3.js.map    434 KiB        admin  [emitted]  admin
    js/application-d351b587b51ad82444e4.js    505 KiB  application  [emitted]  application
js/application-d351b587b51ad82444e4.js.map    569 KiB  application  [emitted]  application
          js/login-1c7b2341998332589ec0.js    385 KiB        login  [emitted]  login
      js/login-1c7b2341998332589ec0.js.map    434 KiB        login  [emitted]  login
                             manifest.json  958 bytes               [emitted]

Apart from generating the fingerprinted files and source map files, it also generates a manifest.json which lists information about all the files generated by the compilation process. Rails uses this file to convert references to the assets in the javascript_pack_tag to the actual compiled files. For eg.javascript_pack_tag('admin') will be converted to js/admin-67dd60bc5c69e9e06cc3.js. A sample manifest.json looks like this.

Now that we have the packs ready, we will use them as per our requirements in the layout files. In my case, the login pack is only used in login layout and is separate from application pack which is used once the user is logged in. For admin layout, apart from application pack, a separate admin pack is used. We can use any of the packs by including it in the layout file.

<body>
  <%= javascript_pack_tag "application" %>
  <%= javascript_pack_tag "admin" %>
</body>

So to summarize:

  • Keep pack files minimal and just import the required code from other files.
  • Only pack files must go in app/javascript/packs
  • You are free to organize the rest of the JavaScript code as per your wish in the app/javascript.
  • Keep an eye on the output of Webpack to monitor the bundle size.
  • Organize pack files as per your requirements and manage the packs depending on the features they will serve.

If you are interested in knowing more about Webpacker and Rails 6, be with me on the Road to Rails 6.