I tried seabass's approach above, and had some difficulty getting it to work. I did find it to be very helpful and informational though. Using his ideas, I was able to create a new Angular 6 application and get application state to persist through HMR builds. I created a project on Github so others can pull it down if they want to experiment with it, as this is the best way to learn. Read code comments and check the console log to gain understanding about the order in which things happen, and how they work.
https://github.com/ermcgrat/NgStarter
Clone the project, do an npm install, and then run application with "npm run start". Try changing code in AppComponent and see how the hmr works.
In short however, I was able to achieve state persistence by creating a state service, and leveraging it in my AppModule and hmrBootstrap. First I started with basic HMR functionality, as specified by the Angular CLI team:
https://github.com/angular/angular-cli/wiki/stories-configure-hmr
This will get HMR working, but it won't persist state. I extended the hmr.ts file to save our state when the module is disposed (unloaded). When the new module is evaluated it will read this saved state from the HMR module and inject it into our new module:
hmr.ts
export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
module.hot.accept();
bootstrap().then(mod => {
// Attach a dispose handler. When this module is replaced, we will first run this code before
// evaluating the new module. (eg. running main.ts)
module.hot.dispose(data => {
if (mod.instance.hmrOnDestroy) {
mod.instance.hmrOnDestroy(data);
}
const appRef: ApplicationRef = mod.injector.get(ApplicationRef);
const elements = appRef.components.map(c => c.location.nativeElement);
const makeVisible = createNewHosts(elements);
mod.destroy();
makeVisible();
});
// Does this module have an hmrOnInit method for us to run?
// And is there state data from previous unloaded module to initalize?
let prevData;
if (module.hot.data && module.hot.data.appState) {
prevData = module.hot.data.appState;
}
if (mod.instance.hmrOnInit && prevData) {
mod.instance.hmrOnInit(prevData);
}
});
};
And here is our AppModule, implementing the hmrOnInit and hmrOnDestroy methods used above. Of note is how the hmrOnInit is restoring application state (if exists) through the state service.
app.module.ts
export class AppModule {
constructor(private appRef: ApplicationRef, private stateService: AppStateService) { }
hmrOnInit(prevState: any) {
if (prevState) {
this.stateService.saveAppState(prevState);
// change detection.
this.appRef.tick();
}
}
hmrOnDestroy(data: any) {
// Here we will increment our hmrBuilds counter, and then save our state to
// data (module.hot.data), so that it will be available to the new module.
const hmrBuilds = this.stateService.getHmrBuilds() + 1;
this.stateService.saveHmrBuilds(hmrBuilds);
data.appState = this.stateService.getAppState();
}
}
And finally the AppStateService. The only thing particularly tricky with this is that we are essentially maintaining the application state in 2 forms. 1 of these is a plain-old-vanilla-object for synchronous access (this is necessary for HMR rebuilds, as async functions in the module dispose can't be guaranteed to finish BEFORE the new module is evaluated). The 2nd is an observable version of our application state, so that various components can easily observe changes/updates to state.
app.state.service.ts
export class AppStateService {
// attach various component states to this object
// We maintain an object for synchronous use by the HMR, and an Observable for use by the application and its templates.
private appState: IAppState = { hmrBuilds: 0 };
private appStateSubject = new BehaviorSubject<IAppState>({ hmrBuilds: 0 });
public appState$: Observable<IAppState> = this.appStateSubject.asObservable();
constructor() { }
public getAppState() {
return this.appState;
}
public getHmrBuilds(): number {
return this.appState.hmrBuilds ? this.appState.hmrBuilds : 0;
}
public saveAppState(newState: IAppState) {
this.appState = newState;
this.appStateSubject.next(newState);
}
public saveHmrBuilds(buildNum: number) {
this.appState.hmrBuilds = buildNum;
}
}
And finally, any application components can now observe this appState$ and use it within their component code or template.
I would also like to note that this approach to maintaining state between HMR builds essentially leads to a single source of truth. It is in my opinion that a state library such as ngrx would perfectly integrat with something like this.