DEV Community

Cover image for Things to watch out for when using HMR with Angular
Dzhavat Ushev for This is Angular

Posted on • Originally published at dzhavat.github.io

Things to watch out for when using HMR with Angular

Angular v11 was released a couple of weeks ago. One of the highlights in this release is making it easier to enable Hot Module Replacement (HMR) during the development of our apps. All we need to do is use the --hmr flag:

ng serve --hmr

To quote the release post:

Now during development the latest changes to components, templates and styles will be instantly updated into the running application. All without requiring a full page refresh. Data typed into forms are preserved as well as scroll position providing a boost to developer productivity.

I was excited to try it! I quickly installed the newest Angular CLI and generated a fresh new app.

My initial reaction was quite positive. HMR works like magic!

But then I began to wonder how will a more complex app behave with HMR enabled? I asked this question in the Angular’s Discord channel and got a really good explanation by Lars Gyrup Brink Nielsen. To quote:

If the application hasn’t been built with Hot Module Replacement in mind from the beginning, it might need some work. The issue with HMR is when application state gets stale or memory leaks occur. This can happen for application- and platform-wide dependencies. We usually don’t think about cleaning up resources such as RxJS subscriptions, open Websockets, and so on at this level. But when we use HMR, the AppModule and all singleton services are asked to be destroyed. If the code doesn’t account for this, the same side effects can be triggered/active multiple times which causes different things to get out of sync.

Really good point!

Enabling HMR requires a different mindset. It emphasizes the need to be careful with long-lived RxJS subscriptions, setInterval functions, WebSockets connections, etc., while developing our apps. On top of that, we must also keep in mind that this behaviour occurs only in development.

Let’s illustrate the problem.

Say I have this code in AppComponent (which is a long-lived component that doesn’t get destroyed throughout the “live” of the app):

@Component({ ... })
export class AppComponent {
  ngOnInit() {
    interval(1000).subscribe(value => {
      console.log('value', value);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Running the app with --hmr enabled will result in this:

Enabling HMR and inspecting the console on initial load

Here I have an RxJS subscription that logs values to the console. The subscription is not cleared but that shouldn’t be a problem since the component is never going to get destroyed. So far everything works as expected.

Now, if I change the code a bit and save the file, the app will not rebuild again and force a full page refresh in the browser, as we’re used to. Rather, it will only rebuild the parts that were modified and replace them in the running app:

Issue when making changes to the code (HMR enabled)

But now the console shows logs from multiple subscriptions. Why is that? It is because of old subscriptions that are still active in the background, effectively creating a memory leak. This would not have been a problem without HMR because the app would’ve been rebuild again and forced full browser page refresh (which in turn destroys all previous subscriptions).

It’s important to emphasize here again that the code above will run as expected in production. There will be only one active subscription. This problem occurs only in development with HMR turned on.

To fix the issue, we must remember to clear the subscription in the ngOnDestroy hook for that component.

@Component({ ... })
export class AppComponent {
  sub: Subscription | undefined;

  ngOnInit() {
    this.sub = interval(1000).subscribe(value => {
      console.log('values', value);
    });
  }

  ngOnDestroy() {
    this.sub?.unsubscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Fixing the issue by clearing the subscription (HMR enabled)

After this change, saving the file multiple times doesn’t result in old subscriptions logging to the console because they are properly cleared.

Summary

I love HMR!

It’s exciting, works great and improves the developer experience. However, it doesn’t come without a cost. Enabling HMR requires a slight change in mindset when developing our applications. We must remember to:

  • clear long-lived RxJS subscriptions
  • clear setInterval functions
  • close WebSocket connections
  • properly manage app- and platform-wide dependencies (like componens and services)

Failing to do so, might result in unexpected results and memory leaks, which can be hard to debug.

Is there something else we should be aware of when HMR is turned on?


Photo by Philip Brown on Unsplash

Top comments (9)

Collapse
 
dzhavat profile image
Dzhavat Ushev

Thanks Stephen.
I agree that HMR is a great feature. I've been working on an Angular app with HMR enabled for a few months now. HMR has probably saved me some valuable time. In terms of doing something because of HMR, I can remember a couple of places where I had to clear a subscription myself in long-lived component(s)/service(s) in order to not introduce memory leaks in development. So that is something one needs to be aware of and actively do something about it. Other than that, as long as subscriptions are handled either by the async pipe or some other way, everything should be working just fine.

Something else I've noticed is that there's some issue when HMR is enabled in a app that uses Angular Material and NgRx but I haven't fully figured it out yet. It's related to those components that use input fields like the date picker. It could as well be an issue on my end. Don't know yet 🤷‍♂️🙂

Collapse
 
anggachelsea profile image
Angga Lesmana

What do u think . If I use a subscribe in service?

Collapse
 
dzhavat profile image
Dzhavat Ushev

There's no problem with that. Services have ngOnDestroy hook as well. You can use it for clean up. coryrylan.com/blog/using-ngondestr...

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

That's good advice. Just don't try to use ngOnInit. It doesn't work for services. Instead, use the service constructor or an Angular initializer.

Collapse
 
charlesr1971 profile image
Charles Robertson

I actually think that HMR is good way to make sure code has been properly crafted. If there are memory leaks, this is not the fault of HMR, but the fault of poorly built code.

Collapse
 
dzhavat profile image
Dzhavat Ushev

I see HMR going both ways. It's definitely a good idea to properly clean subscriptions but in some cases it's done only to please the tooling and not because of a bigger benefit. But I'm definitely enabling HMR in all my apps. It has saved me a lot of hours which would've went waiting for my app to simply reload.

Collapse
 
nofpowells profile image
Luiz Gabriel

Thanks dzhavat.
You can use HMR with lazy modules?

Collapse
 
dzhavat profile image
Dzhavat Ushev

Hey Luiz,
Yes, it's working with both lazy and non-lazy modules. But keep in mind that HMR is only used during development to make the developer experience (DX) better.

Collapse
 
carlosds profile image
Karel De Smet

Great post, just what I was looking for. I'm gonna give HMR a try myself, looks interesting and when you have complex flows, it can be a time-saver not having to reload all the time.

Thanks!