Hot Module Replacement with angular/cli

Shlomi Levi
4 min readJul 11, 2017

--

This post is about Angular 4 & Angular/cli v1.2.0

Hot Module Replacement aka HMR aka module live reload is a way of exchanging modules in a running application (and adding/removing modules). You basically can update changed modules without a full page reload.

Angular/Cli allow you to use HMR option by running ng serve --hmr command. but you will need HMR additional code to integrate between hmr to your application which is not include by default.

You can read more about HMR by visiting this page.

Let’s make HMR and angular/cli working together!

Okay, Let’s set up a new environment for HMR.
Why? Because you can choose to use HMR or not. for example in prod environment you’ll find that HMR is useless and expensive operation.

First we need to tell our code that we want to set hmr to true and we are not in production mode. Create the file src/environments/environment.hmr.ts with the following content:

export const environment = {
production: false,
hmr: true
};

add hrm: false flag to the existing file src/environments/environment.ts

export const environment = {
production: false,
hmr: false,
...
};

add hrm: false flag to the file src/environments/environment.prod.ts

export const environment = {
production: true,
hmr: false,
...
};

update your.angular-cli.json file by adding hmr environment to the existing environments section:

"environmentSource": "environments/environment.ts",
"environments": {
...
"hmr": "environments/environment.hmr.ts",
}

Now, for telling our application that we in HMR environment, we need to set (e)nvironment flag to hmr. why? we’ll see it later. Create a shortcut for this by updating package.json and adding an entry to the script section:

"scripts": {
...
"hmr": "ng serve --hmr -e=hmr"
}

Install the package from angularclass to support hmr:

$ npm install --save-dev @angularclass/hmr
# or if you using Yarn
$ yarn add @angularclass/hmr

Okay, now let’s add HMR additional application code:

Create a file called src/hmr.ts and import classes from core and hmr:

import { NgModuleRef, ApplicationRef } from '@angular/core';
import { createNewHosts } from '@angularclass/hmr';

We export a function that handle our replacement modules. our expect arguments is the current module and angular bootstrap function.

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {

We need to call accept()which track our current module.

module.hot.accept();

After that we bootstrap our application, but we keep a reference to the current bootstrap module.

let ngModule: NgModuleRef<any>;
bootstrap().then(currentModule => ngModule = currentModule);

Now, when we make some changes in our code the dispose function called.

module.hot.dispose(() => {
....
});

in our dispose function we need to get the applicationRef from our bootstrap module. so every components in this module we want to getting the elements inside and recreate root elements for bootstrapping using createNewHosts()

const appRef = ngModule.injector.get(ApplicationRef);
const elements = appRef.components.map(c =>
c.location.nativeElement);
const removeOldHosts = createNewHosts(elements);

createNewHosts() return a function that remove old hosts. this function cached components and insert/remove specific elements to the DOM using parentNode.removeChild/parentNode.insertBefore methods.

ngModule.destroy();
removeOldHosts();

Your src/hmr.ts should have the following content:

import { NgModuleRef, ApplicationRef } from '@angular/core';
import { createNewHosts } from '@angularclass/hmr';

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
let ngModule: NgModuleRef<any>;
module.hot.accept();
bootstrap().then(currentModule => ngModule = currentModule);
module.hot.dispose(() => {
const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
const elements = appRef.components.map(c => c.location.nativeElement);
const removeOldHosts = createNewHosts(elements);
ngModule.destroy();
removeOldHosts();
});
};

Update src/main.ts to use the file we just created:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

import { hmrBootstrap } from './hmr';

if (environment.production) {
enableProdMode();
}

const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule);

if (environment.hmr) {
if (module[ 'hot' ]) {
hmrBootstrap(module, bootstrap);
} else {
console.error('Ammm.. HMR is not enabled for webpack');
}
} else {
bootstrap();
}

The main file it’s simple. we caching bootstrapModule function that we will pass to our hmrBootstrap function if hmr flag is true. otherwise we’re just execute that.

Before we call hmrBootstrap we need to know if hmr is enabled by the webpack. For this we have module variable (node module which declared in src/typings.ts). it’s just metadata about current module in our webpack execution.

Start your application with HMR!

Now everything is ready for running in hmr mode:

$ npm run hmr

When you make changes in your code they changes should be visible automatically without a complete browser refresh.

If you got warning cli message don’t worry:

It’s because regardless to your application code that handle this, when hmr is triggered the Cli is write to you to be aware about HMR module as you can see in the angular/cli source code:

You can find the full source code here.

Follow me on Medium or Twitter to read more about Angular!

--

--

Shlomi Levi

I'm independent consultant, mentor, coder, make some magic and providing Angular/Node.JS consulting to ambitious teams.