Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to preserve state during HMR using Angular

In Angular, is there a way to preserve application state after a module has been hot reloaded? Similar to what happens in VueJS:

VueJS HMR Example

So far I've gotten HMR to work following several tutorials, but all it does is reload the app without doing an actual page refresh. Fasterthe a full load, yes. But still not where it could be.

Has anyone gotten this to actually work?

PS: it relates to https://github.com/beeman/tutorial-angular-cli-hmr/issues/4

like image 290
Shane Avatar asked Apr 01 '18 07:04

Shane


People also ask

What is the use of HMR in Angular?

HRM is Webpack feature which is also used by Angular11 team since Angular build system is Webpack based. HRM feature enables development faster because it updates limited resource after you save code in project.

Does Angular have hot reload?

Angular (and really any of the major JavaScript UI frameworks that integrate with the WebPack Dev Server) provides a built-in Live Reload Web server that makes it quick and easy to see UI changes updated in the browser as soon as you make a change to your code, HTML or CSS content.

How do you use HMR in Angular 12?

In order to enable HMR during the build and serve processes, configurations must be added. Note: Replace “angular-hmr-app” with the name of your application. Our project will not work with HMR unless we integrate its dependencies. You can install the @angularclass/hmr module by running the following command.

How do I enable HMR in Angular XI?

Install the required hmr angular package. Add 'node' into the types array in compilerOptions in src/tsconfig. app. json file.


1 Answers

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.

like image 134
emcgrath Avatar answered Oct 19 '22 23:10

emcgrath