Why You Should Use ngOnDestroy in Angular

Why You Should Use ngOnDestroy in Angular

In Angular, lifecycle hooks are methods that execute at specific points during a component’s lifecycle—from initialization to updates and eventual destruction. To control component behavior at these key points, we implement specific lifecycle hook methods.

For example, the ngOnInit hook runs upon component initialization. It’s commonly used for setup tasks, like fetching data or configuring settings:

When the component above is initialized, it will log the text “initialized” to the console.

The ngOnDestroy Hook

The ngOnDestroy hook runs just before Angular destroys the component. It’s intended for cleanup tasks, such as unsubscribing from observables or stopping timers:

The component above logs “destroyed” to the console right before it is removed from the view.

Although the purpose of ngOnDestroy is straightforward, many developers overlook it or forget to use it altogether. Even when ngOnDestroy is implemented, developers sometimes fail to unsubscribe from all observables or stop timers. This often happens due to tight deadlines or because skipping ngOnDestroy doesn’t usually cause immediate or obvious issues. Components typically continue to render, navigate, and update as expected, so the effects of not using ngOnDestroy might not be noticeable right away.

However, without ngOnDestroy, issues like memory leaks, increased network calls, and unexpected behavior can gradually accumulate, especially in applications with heavy navigation or dynamically created components. These problems often surface later as performance degradation, inconsistent data, or erratic app behavior, making it difficult to trace the cause back to a missing ngOnDestroy.

In the rest of this article, we’ll simulate scenarios where ngOnDestroy should be used for cleanup but isn’t, observe the resulting behavior, and then compare it to cases where ngOnDestroy is implemented correctly to show why it’s essential.

Setting Up the Angular Project

Let’s start by bootstrapping a new Angular project. If you haven’t already, install the Angular CLI (Command Line Interface) globally on your machine:

npm install -g @angular/cli

Use the ng new command to create a new Angular project. Replace my-app with your preferred project name:

ng new my-app

This will prompt you with several questions to configure your project, such as whether to include routing and which stylesheet format to use (CSS, SCSS, etc.). Choose the options that best fit your needs.

Once the project is created, navigate into the project folder:

cd my-app

Run the application locally with the ng serve command:

ng serve

This will start a development server, and you can open your browser and visit http://localhost:4200 to view your new Angular project in action.

Memory Leak: Accumulating Subscriptions

A memory leak occurs when a component subscribes to an observable but doesn’t unsubscribe. When the component is destroyed (e.g., through routing), the subscription persists and holds onto the component’s memory. Let’s create a component that subscribes to an observable (interval) but doesn’t unsubscribe:

Create a MemoryLeakComponent at src/app/memory-leak.component.ts with the following code:

This component subscribes to an interval that triggers every 1000 milliseconds (1 second), increments the count every second, and logs the count to the console, as well as displays it in the component’s UI.

Next, update the app’s routes at src/app/app.routes.ts to include a route for the MemoryLeakComponent:

Then, update src/app/app.component.html to include a button that navigates to the "memory-leak" route:

In src/app/app.component.ts, define the navigate method:

When we load the app and navigate to the MemoryLeakComponent, the count is displayed and updated. When we click “Go back”, the component is destroyed. Upon returning to the "memory-leak" route, the counter resets to 0, and everything seems to work as expected. At this point, we might assume that not using ngOnDestroy doesn’t cause any issues.

However, let’s observe the behavior more closely. From the "/" route, open the browser’s console and refresh. Navigate to the "memory-leak" route, and the count will log correctly. Now, go back to the "/" route. Even though the MemoryLeakComponent is no longer visible, the count is still being logged in the console. This means the interval subscription continues to run even after the component has been destroyed.

When we return to the "memory-leak" route, a new subscription starts, even though the previous one is still running. We now have multiple counters running simultaneously:

If we repeat this multiple times, new subscriptions are created each time, leading to multiple simultaneous subscriptions. If the logic inside the interval is heavy, this can have a significant impact on the app’s memory.

As developers, it’s easy to overlook this issue when the main functionality behaves correctly. However, the accumulating subscriptions can cause significant performance problems.

Now, let’s fix the memory leak by unsubscribing from the observable when the component is destroyed:

With this fix, when we navigate to the "memory-leak" route and go back to "/", the console logs “Unsubscribed and cleaned up”, and no count continues running. If we navigate back to the "memory-leak" route, the count starts afresh, both in the UI and console, with no lingering subscriptions.

Other Scenarios Where ngOnDestroy is Necessary

While we focused on subscription cleanup in the previous example, the ngOnDestroy lifecycle hook is crucial for handling many other scenarios that involve resource cleanup or teardown. Without properly using ngOnDestroy, several issues can arise that negatively affect application performance and behavior.

Here are some other scenarios and the problems that can occur when ngOnDestroy is not implemented:

  • Repeated Event Listeners: Event listeners attached to DOM elements or other events (e.g., window, document) can continue running even after a component is destroyed. If these listeners are not removed, they can lead to memory leaks, performance issues, and unexpected behavior, as they may continue to execute logic or access outdated component data. The app may continue to respond to events like scroll or resize, causing unnecessary computations and UI updates even when the component is no longer visible.
  • Conflicting Data Changes After Component Destruction: If a component still listens for or attempts to modify data after it has been destroyed, it can lead to conflicts or inconsistent states. For example, an HTTP request might still be processing, and the response could attempt to update data on a component that no longer exists, leading to errors. And even when there are no errors, it could result in data overwrites, or undefined states that may not be easily detected, causing data integrity issues.
  • Unnecessary API Requests: Components that initiate API calls (e.g., through services or observables) might still send requests after they’ve been destroyed if not properly unsubscribed. These unnecessary requests can increase server load, waste bandwidth, and degrade performance.
  • Unstopped Animations After Component Destruction: If animations are not properly destroyed when a component is removed, they may continue running in the background. This can create visual glitches, such as lingering transitions or effects that don’t align with the current state of the application.

Conclusion

In this article, we highlighted the importance of using the ngOnDestroy lifecycle hook to manage resources in Angular components. Proper cleanup of subscriptions, event listeners, timers, and other resources prevents memory leaks and performance issues. By implementing ngOnDestroy, developers can avoid problems like unnecessary API calls and unexpected behavior, ensuring a more stable and efficient application. Proper resource management is key to building reliable Angular applications.

Leave a Reply

Your email address will not be published. Required fields are marked *