HMR and Angular: Pitfalls to Keep in Mind

Angular v11 shipped not long ago, and one of its headline features is simplifying the setup for Hot Module Replacement (HMR) in development. Just pass the --hmr flag:

ng serve --hmr

From the official release notes:

During development, the most recent modifications to components, templates, and styles are now instantly applied to the running application. No full page reload is necessary. Form input and scroll position are preserved, giving developers a significant productivity lift.

Naturally, I was eager to give it a spin. I grabbed the latest Angular CLI, scaffolded a brand-new project, and was immediately impressed. HMR genuinely feels like magic.

That initial excitement, though, led me to a deeper question: how would a real-world, more intricate application hold up under HMR? I raised this on the Angular Discord server and received a thoughtful reply from Lars Gyrup Brink Nielsen. His point:

If an application wasn’t architected with Hot Module Replacement in mind from day one, it might take some adjustment. The main challenges with HMR are stale application state and potential memory leaks. These often arise from dependencies that are application-wide or platform-wide. We don’t typically stop to think about tearing down resources such as RxJS subscriptions, open WebSockets, and similar items at that level. Yet, when HMR is active, the AppModule and all singleton services face destruction. If the code doesn’t handle this cleanup, the same side effects can be triggered repeatedly or remain active multiple times, leading to a cascade of synchronization problems.

An excellent observation!

Turning on HMR calls for a shift in how we think. It forces us to be vigilant about long-running RxJS subscriptions, setInterval timers, WebSocket connections, and similar constructs while we code. It’s also crucial to remember this is a development-only concern.

Let’s walk through a practical example to make this concrete.

Consider this snippet in AppComponent, a long-lived component that normally survives for the entire application session:

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

When you boot the app with --hmr, here’s what you’ll observe:

Enabling HMR and inspecting the console on initial load

In this case, an RxJS subscription logs values to the console. There’s no explicit teardown, but that’s intentional since the component is never supposed to be destroyed. Initially, everything behaves as expected.

Now, make a small code change and save the file. Rather than triggering a full rebuild and browser refresh, HMR swaps out only the modified pieces in the live application:

Issue when making changes to the code (HMR enabled)

But wait — the console now shows log entries from multiple subscriptions. Why? Because the original subscription is still alive in the background, alongside the new one. This duplication is effectively a memory leak. Without HMR, a code change would have caused a full rebuild and a page reload, which would have cleared out the old subscription along with everything else.

It’s worth reiterating: the code as written works perfectly fine in production. Only one subscription would exist. This duplication issue is exclusive to development mode with HMR enabled.

The remedy is straightforward: make sure to unsubscribe in the ngOnDestroy lifecycle 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)

With that fix in place, repeated saves no longer produce duplicate console logs, as the previous subscriptions are always properly disposed of.

Wrapping Up

I’m a big fan of HMR!

It’s impressive, performs admirably, and significantly enhances the development workflow. However, it introduces new responsibilities. Adopting HMR means adjusting your development habits. You’ll need to be disciplined about:

  • tearing down long-lived RxJS subscriptions
  • clearing setInterval timers
  • closing WebSocket connections
  • managing application- and platform-level dependencies, such as components and services, with care

Neglecting these can lead to unpredictable behavior and memory leaks that are notoriously difficult to track down.

Are there other gotchas you’ve encountered with HMR? I’d like to hear about them.


Photo by Philip Brown on Unsplash