Skip to content

Integrating webpack-dev-server + hot module replacement with Django

How to wire webpack-dev-server into a Django project to get hot module replacement, including a template tag that switches between development and production.

4 min read
  • webpack
  • django
  • tooling

Originally published on Medium

TL;DR: I won't go too deep into the concepts used here, since I assume you have some experience with webpack and Django. Still, at the end of the post I'll leave a few links you can use to dig deeper.

I have to assume that if you landed on this post, it's because you're looking for a quick and simple way to improve the front-end development experience… and that you obviously already use webpack and Django ლ(╹◡╹ლ)

To keep it simple, webpack-dev-server is a tool that automatically refreshes the browser whenever you change your JavaScript code. To communicate these changes to the browser, webpack-dev-server uses an express.js server configured with socket.io.

A couple of things to keep in mind:

Live reload: webpack-dev-server's default behavior; when you change the code, the browser automatically does a full reload.

Hot Module Replacement (HMR): with a couple of plugins and some extra configuration, our changes get reflected in the browser without a full reload.

Let's get right to it…

Configuring webpack

First install the package:

npm install --save-dev webpack-dev-server

Then let's change our webpack configuration:

module.exports = {
  // ... the rest of your configuration
  entry: {
    app: './static/js/app.js',
  },
  output: {
    // output.publicPath must match the devServer publicPath
    publicPath: 'http://localhost:9000/static/js/',
    filename: '[name].min.js',
  },
  // devServer is what we need to configure to get hmr with webpack-dev-server
  devServer: {
    // public url our files will be served from
    publicPath: 'http://localhost:9000/static/js/',
    // port webpack-dev-server will run on (default is 8080)
    port: 9000,
    // TURN ON HMR
    hot: true,
    /*
      the headers configuration is needed to tell webpack to accept
      requests from any port (in this case, the requests our django server
      will make to fetch the js files). otherwise, when we change our code,
      the browser console will show the following message:
      "No 'Access-Control-Allow-Origin' header is present on the requested resource"
    */
    headers: {
      /*
        we could also set the exact url our Django server runs on:
        'Access-Control-Allow-Origin': 'http://localhost:8000'
        but then urls like "http://my-localhost-alias:8000" or "http://my-public-ip:8000"
        wouldn't be able to access our js files, since they don't contain
        the specified domain (localhost).
      */
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET,OPTIONS,HEAD,PUT,POST,DELETE,PATCH',
      'Access-Control-Allow-Headers':
        'Origin, Content-Type, Accept, Authorization, X-Request-With',
      'Access-Control-Allow-Credentials': 'true',
    },
    // for the rest of the options, check:
    // https://webpack.js.org/configuration/dev-server/#devserver
  },
  plugins: [
    // ...your other plugins
    // plugins required to enable hmr
    // new webpack.NoEmitOnErrorsPlugin(), // in webpack v4 this plugin is enabled by default
    new webpack.HotModuleReplacementPlugin(),
  ],
};

As you can see, it's nothing complicated: we just configure the devServer and add a couple of plugins to get HMR. Now we only need a new script in our package.json to run webpack-dev-server:

"client:hmr": "webpack-dev-server --colors --progress"

Run the command in a terminal and that's it — 70% of the work is done:

npm run client:hmr

Configuring Django

What we normally have in our Django templates is something like this (assuming your static files are served at http://localhost:8000/static/**/*):

<script src="{% static 'js/app.min.js' %}"></script>

But now that webpack-dev-server is running on port 9000, all our scripts must point to that port, so let's change the script to:

<script src="http://localhost:9000/static/js/app.min.js"></script>

Yes, http://localhost:9000/static/js is the value we set in output.publicPath and devServer.publicPath. And yes, you can use any path you want (╯°□°)╯ — I chose this one to stay consistent with how our Django app serves static files.

At this point, if you open the configured template's URL, you'll see the following logs in the browser console:

HMR logs in the browser console

We did it…

Success

…but it's not quite right ( ゚,ゝ゚). We're not trying to be perfect, but we do want to do it as well as possible, right? ┌( ಠಠ )┘

The main problem is that to ship this to production we'd have to 'manually' change the script URLs, and then change them back again to return to development mode.

The solution: create a template tag that switches between production and development mode for us.

For that, I recommend adding variables to settings.py so we can reuse them wherever we need:

WEBPACK_DEV_SERVER_URL = 'http://localhost:9000'
WEBPACK_DEV_SERVER = True

We use them when creating our webpack template tag:

# ../templatetags/webpack_tags.py
# I'm no Django expert since I'm more of a front-end person than
# back-end, but this is how I'd do it (ノ ಥ ウಥ )ノ
from django import template
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
 
register = template.Library()
 
 
@register.simple_tag
def webpack(javascript_file, **args):
    if settings.DEBUG and settings.WEBPACK_DEV_SERVER:
        return '%s/%s' % (settings.WEBPACK_DEV_SERVER_URL, javascript_file)
    return static(javascript_file)

Now let's go to our template and change the script:

{% load webpack_tags %}
 
<script src="{% webpack 'app.min.js' %}"></script>

Refresh your browser and you should see the HMR logs:

HMR logs working

We made it (屮◉◞益◟◉)屮

Well done, soldier

Obviously you still need to enable HMR inside your JavaScript files, but that's relatively easy, so you shouldn't have any trouble doing it yourself — you wanted me to show you that too? Sorry kid, life isn't that easy ლ(ಠ益ಠლ). Here's the official guide.

And that's all! ( ̯͡◕ ▽ ̯͡◕ ) Thank you so much for reading. If you found it useful, give it a good 👏 and share it with your friends — I'll be very grateful if you do (•̪◡•̪)

Any feedback, correction or improvement is welcome!!

Helpful links: