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.