Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Hot Module Replacement

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

If you read the documentation about the webpack-dev-server, you'll read about HMR: heavy metal rock. Or, actually, in this context, it means hot module replacement. HMR is amazing: it allows Webpack to update your page... without refreshing! But... you need to configure your system correctly to be able to do this. And, unless you do even more setup, it only works for CSS. You can make it work for JavaScript - especially if you're using a framework like React or Vue, but you won't get it out of the box.

Anyways, let's get HMR working for CSS at least.

Activating HMR: --hot

To activate it, stop the dev server and re-run it with a --hot flag... because HMR is the hotness... and also because it stands for hot module replacement:

./node_modules/.bin/webpack-dev-server --hot

Go refresh the browser to reset things. Now, make a change to our CSS and move back to the browser:

$brand-primary: darken(#428bca, 2.5%); // #3885c7
... lines 2 - 82

Huh, it still refreshed. It's not working!

Webpack, AJAX and publicPath

Go to your browser's console, click the gear icon and check "Preserve logs". Thanks to this, the console will not clear logs when the page reloads.

Refresh once more... wait for the page to load... and then clear the console. Go make another change to the app, then move back. It says "App updated", "Recompiling", but then it refreshes the whole page!

These messages are coming from the HMR system. And check this out: it says "Checking for updates on the server", and then you see a failed request to http://localhost:8000. Finally, it says "Cannot find Update" and it refreshes the page.

What's going on? In addition to our built assets, the dev server - so localhost:8080 - serves a JSON file - this *.hot-update.json - with information about what was just updated.

So, why is that failing? Well, since our site actually lives at localhost:8000, when Webpack makes an AJAX call for this file, it makes it to localhost:8000 instead of localhost:8080. Internally, Webpack doesn't know that all of our assets are being hosted on some other host!

This is actually more important than just the dev server. Sometimes - like with a cool feature called code splitting that we'll see later - Webpack needs to make an AJAX call back to the server. In order to do this, it must know what the public URL is to your assets. Actually, we've already set this in webpack.config.js. In the output section, we set publicPath to /build/:

119 lines webpack.config.js
... lines 1 - 30
module.exports = {
... lines 32 - 36
output: {
... lines 38 - 39
publicPath: '/build/'
},
... lines 42 - 117
};

So... what's the problem? That is our public path, right? Not anymore! If you're using the webpack-dev-server - or if you're using a CDN on production - this publicPath must be absolute: it needs to include the host name where your assets are really stored. This is used for any Webpack AJAX calls and also to point to assets, like font files from inside CSS. That's why we saw some font file 404's in the last chapter.

Absolute publicPath

Let's fix this! Above the config, set two new variables. First, set useDevServer to true:

125 lines webpack.config.js
... lines 1 - 4
const useDevServer = true;
... lines 6 - 32
module.exports = {
... lines 34 - 123
};

That will allow us to easily turn this off later. Then, add const publicPath =. If devServer is true, set this to http://localhost:8080/build/. Otherwise, use the normal /build/:

125 lines webpack.config.js
... lines 1 - 4
const useDevServer = true;
const publicPath = useDevServer ? 'http://localhost:8080/build/' : '/build/';
... lines 7 - 32
module.exports = {
... lines 34 - 123
};

Down below, use this variable for publicPath:

125 lines webpack.config.js
... lines 1 - 4
const useDevServer = true;
const publicPath = useDevServer ? 'http://localhost:8080/build/' : '/build/';
... lines 7 - 32
module.exports = {
... lines 34 - 38
output: {
... lines 40 - 41
publicPath: publicPath,
},
... lines 44 - 123
};

Solving webpack-dev-server CORS problems

And while we're here, scroll down to devServer and add one new key called headers. Add a single header: Access-Control-Allow-Origin set to *:

125 lines webpack.config.js
... lines 1 - 4
const useDevServer = true;
const publicPath = useDevServer ? 'http://localhost:8080/build/' : '/build/';
... lines 7 - 32
module.exports = {
... lines 34 - 38
output: {
... lines 40 - 41
publicPath: publicPath,
},
... lines 44 - 119
devServer: {
... line 121
headers: { 'Access-Control-Allow-Origin': '*' },
}
};

Since our site is served on a different host than our assets... well really a different port... CORS security will prevent some requests from working. This header will allow those requests to be made.

Phew! Restart the dev server one more time. Then, manually refresh the page to reset everything. Clear out the log messages.

Ready for this? Go back to main.scss, make a change, then come back:

$brand-primary: darken(#428bca, 2.5%); // #3885c7
... lines 2 - 82

It says "App updated", "Recompiling"... then a few other things. But... it did not refresh! Did it work? I'm not sure! Let's make a more dramatic change: how about 2% to 99%:

$brand-primary: darken(#428bca, 2.5%); // #3885c7
... lines 2 - 82

Move back to your browser. Wow! The button turns nearly black... without reloading! This is HMR.

But, as I mentioned earlier, this will only work for CSS. You can get this to work with JavaScript - especially if you're using a front-end framework, But, that's beyond this tutorial.

Next! Let's move our app towards being ready for production, by fixing the annoying CSS delay when we refresh.

Leave a comment!

1
Login or Register to join the conversation
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 4 years ago

that is cool, if I would be often working with css and JS, I would want it. Often pages are heavy, so to reload all page, takes lot of time,, especially in dev environment

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial explains the concepts of an old version of Webpack using an old version of Symfony. The most important concepts are still the same, but you should expect significant differences in new versions.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.3.*", // v3.3.16
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.0
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.3
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.4.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "^2.0", // v2.1.2
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.3.5
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": [],
    "devDependencies": {
        "babel-core": "^6.25.0", // 6.25.0
        "babel-loader": "^7.1.1", // 7.1.1
        "babel-plugin-syntax-dynamic-import": "^6.18.0", // 6.18.0
        "babel-preset-env": "^1.6.0", // 1.6.0
        "bootstrap-sass": "^3.3.7", // 3.3.7
        "clean-webpack-plugin": "^0.1.16", // 0.1.16
        "copy-webpack-plugin": "^4.0.1", // 4.0.1
        "core-js": "^2.4.1", // 2.4.1
        "css-loader": "^0.28.4", // 0.28.4
        "extract-text-webpack-plugin": "^3.0.0", // 3.0.0
        "file-loader": "^0.11.2", // 0.11.2
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.2.1", // 3.2.1
        "lodash": "^4.17.4", // 4.17.4
        "node-sass": "^4.5.3", // 4.5.3
        "resolve-url-loader": "^2.1.0", // 2.1.0
        "sass-loader": "^6.0.6", // 6.0.6
        "style-loader": "^0.18.2", // 0.18.2
        "sweetalert2": "^6.6.6", // 6.6.6
        "webpack": "^3.4.1", // 3.4.1
        "webpack-chunk-hash": "^0.4.0", // 0.4.0
        "webpack-dev-server": "^2.6.1", // 2.6.1
        "webpack-manifest-plugin": "^1.2.1" // 1.2.1
    }
}
userVoice