Angular 6, HMR + NGRX/Store

Marian Benčat
3 min readJun 12, 2018

--

There are several tutorials how to make HMR working with angular on internet. If you wanna use NGRX state management (similar to redux) you would like to persist your state between hot module reloads. So you will spent time searching the internet to find really working solution to combine Angular + HMR + NGRX/Store.. and you will be successfull, if you do not want to use latest Angular. Even Angular developers themselves have several issues in their tutorials.

Lets set your project to use Angular 6, Hot module replacement and NGRX/Store 6 with persisted state working.

This tutorial is compatible with:

  • @angular 6
  • @ngrx/store 6.0.1
  • @angularclass/hmr 2.3.1

Let’s have pretty average application with Angular 6 and ngrx/store installed.

  • Install @angularclass/hmr

npm install @angularclass/hmr — save

Edit your angular.json file — add new HMR configuration:

"configurations": {
.....
"hmr": {"hmr": true,"fileReplacements": [{"replace": "src/environments/environment.ts","with": "src/environments/environment.hmr.ts"}]},....

Create new environment.hmr.ts file :

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

and add hmr: false to other environment files.

Add src/hmr.ts:

import { NgModuleRef, ApplicationRef } from '@angular/core';import { createNewHosts, hmrModule } from '@angularclass/hmr';export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {let ngModule: NgModuleRef<any>;module.hot.accept();bootstrap().then((mod) => {ngModule = mod;return hmrModule(mod, module); // this line is missing in Angular 6 HMR story});module.hot.dispose(() => {const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);const elements = appRef.components.map(c => c.location.nativeElement);const makeVisible = createNewHosts(elements);// ngModule.destroy(); - "has been already destroyed error - in NG6"makeVisible();});};

Ędit main.ts

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('HMR is not enabled for webpack-dev-server!');console.log('Are you using the --hmr flag for ng serve?');}} else {bootstrap();}

Ądd hooks to main module + metareducer to set state after reload:

import { NgModule, ApplicationRef } from '@angular/core';import { removeNgStyles, createNewHosts, bootloader, createInputTransfer } from '@angularclass/hmr';import { AppComponent } from './app.component';import { SharedModule } from '@shared/shared.module';import { CoreModule } from '@core/core.module';import { StoreModule, ActionReducer, Action, MetaReducer, Store } from '@ngrx/store';import { testReducer } from '@shared/reducers/test.reducer';import { take } from 'rxjs/operators';export function stateSetter(reducer: ActionReducer<any>): ActionReducer<any> {return function (state: any, action: any) {if (action.type === 'SET_ROOT_STATE') {console.log(action.payload);return action.payload;}return reducer(state, action);};}export const xyz: MetaReducer<{ count: any }, Action>[] = [stateSetter];@NgModule({declarations: [AppComponent],imports: [CoreModule,StoreModule.forRoot({ count: testReducer }, { metaReducers: xyz }),SharedModule],providers: [],bootstrap: [AppComponent]})export class AppModule {constructor(public appRef: ApplicationRef, private _store: Store<any>) { }createNewHosts(cmps) {const components = Array.prototype.map.call(cmps, function (componentNode) {const newNode = document.createElement(componentNode.tagName);const currentDisplay = newNode.style.display;newNode.style.display = 'none';if (!!componentNode.parentNode) {const parentNode = componentNode.parentNode;parentNode.insertBefore(newNode, componentNode);return function removeOldHost() {newNode.style.display = currentDisplay;try {parentNode.removeChild(componentNode);} catch (e) { }};} else {return function () { }; // make it callable}});return function removeOldHosts() {components.forEach(function (removeOldHost) { return removeOldHost(); });};}hmrOnDestroy(store) {const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);this._store.pipe(take(1)).subscribe(s => store.rootState = s);store.disposeOldHosts = this.createNewHosts(cmpLocation);  store.restoreInputValues = createInputTransfer();removeNgStyles();}hmrAfterDestroy(store) {store.disposeOldHosts();delete store.disposeOldHosts;}hmrOnInit(store) {if (!store || !store.rootState) {return;}// restore state by dispatch a SET_ROOT_STATE actionif (store.rootState) {this._store.dispatch({type: 'SET_ROOT_STATE',payload: store.rootState});}if ('restoreInputValues' in store) { store.restoreInputValues(); }//this.appRef.tick();  <<< REMOVE THIS LINE, or store will not work after HMRObject.keys(store).forEach(prop => delete store[prop]);}}

Run as

ng serve --configuration hmr --hmr

IF U USE REDUX DEVTOOLS CHANGE TO “AUTOSELECT INSTANCES” IN DROPDOWN

Credits

https://github.com/gdi2290/angular-hmr

https://github.com/ngrx/platform

--

--