By Ruslan Prytula January 16, 2017 8:02 PM
Webpack: hot reload for CSS modules

In this post, I’ll show you a way to make hot reloading work for stylesheets in a non-React project with enabled CSS modules. I’ll use SASS as my CSS preprocessor, but it’s going to be pretty much the same for other ones. By the way, it’s going to be a hackish way to get things done :)

Stylesheets in webpack

I am not going to give you a full overview for this topic as it’s already covered in the documentation for webpack. I’ll just highlight the important stuff, so we have something to start with. To make webpack understand SASS you need to install the following loaders:

npm i style-loader css-loader sass-loader --save-dev

and update your config-file:

// webpack.config.js
...
module: {
  loaders: [{
    test: /\.scss$/,
    loaders: ['style', 'css', 'sass']
  }]
}
...

Since style-loader already has a built-in support for HMR, you don’t need any extra steps to make it work. Just include your CSS file, change it and new styles will be applied without a full-page reload.

CSS modules in webpack

Once css-loader is installed, CSS modules could be enabled / disabled using the modules parameter. For example:

// webpack.config.js
...
module: {
  use: [{
    test: /\.css$/,
    loaders: ['style', 'css?modules', 'sass']
  }]
}
...

When CSS modules are enabled, you are able to write your code like this:

// components/helloWorld.js

let style = require('./helloWorld.scss')

window.onload = () => {
  document.body.innerHTML = `<div class="${style.component}">hello world</div>`
}
/* components/helloWorld.scss */

.component {
  padding: 20px;
  background-color: gold;
  color: #fff;
}

From now on, your HMR is broken, because components/helloWorld.scss is not just a stylesheet file anymore, it’s a module that exports a JS object.

The solution

Knowing that webpack has an API to define a custom hot-reload handler, which is called when a module is updated, allows us to check what part of the application has been changed (CSS or JS). For changes in CSS we simply ask browser to fetch the newly generated CSS file, as for changes in JS, we’ll just reload the page. Here is how it looks like in the code:

// your-main-entry-file.js
...

if(module.hot) {
  const FILE_NAME = 'bundle.css'
  let file = ''
  let el = document.querySelector(`link[href*="${FILE_NAME}"]`)
  let {href} = el

  function httpGet(url, callback) {
    let xhr = new XMLHttpRequest()

    xhr.addEventListener('load', () => callback(xhr))
    xhr.open('GET', url)
    xhr.send()
  }

  module.hot.accept()
  module.hot.dispose(() => {
    let url = `${href}?d=${new Date().getTime()}`

    httpGet(url, ({responseText}) => {
      if(responseText == file)
        window.location.reload() // js was changed
      else
        el.href = url
    })
  })

  httpGet(href, ({responseText}) => {
    file = responseText
  })
}

Please note, that this solution will work only when extract-text-webpack-plugin is enabled in your webpack configuration, which is not a problem, cause having your dev configuration consistent with production one, is surely a good thing that helps to avoid an unexpected behavior. A fully working example could be found here.

Conclusions

A skilled reader might propose using require.context to detect the changes in a SASS file and reload the stylesheet instead of making an additional request to the server. The problem with this approach is that it doesn’t work with some loaders, e.g: sass-resources-loader. Thanks for reading!