Published
- 11 min read
TakeUntilDestroyed in Angular
Subscriptions in Angular 22+
In Single Page Applications lots of data are loaded asynchronously in various ways. Sometimes we just need Data once, other times we want to stay informed (through a subscription) on what is happening for as long as we have the page open.
The Problem
If you subscribe to an observable inside a component, you need to make sure to unsubscribe when the component is destroyed. Otherwise the subscription keeps running in the background and you end up with a memory leak.
This is one of the most common sources of bugs in Angular applications. When a component subscribes to an observable (like a timer, a Subject or an Observable), the subscription remains active even after the component is destroyed. This can lead to: Memory leaks (Subscriptions keep consuming memory and resources), Unexpected behavior (Old subscriptions continue to update state in destroyed components), Performance degradation (Multiple overlapping subscriptions accumulate over time)
This post explores different approaches to solve this problem, from manual unsubscription to modern RxJS operators.
1. Setup
This section establishes a complete example that we’ll use to demonstrate the subscription problem and progressively improve it through different solutions.
Data Service
We’ll create a DataService that fetches data from an external API. This service represents any data source in your Angular application (REST API, GraphQL, WebSocket, etc.):
// data.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private http = inject(HttpClient);
private apiUrl = 'https://api.thedogapi.com/v1/images/search?limit=2';
getDogs(): Observable<unknown> {
return this.http.get<unknown>(this.apiUrl);
}
}
A container component
We need a parent component that can dynamically create and destroy our timer component so we can observe subscription behavior:
<!-- takeuntil-example.html -->
<button (click)="timerIsActive() ? hideTimer() : showTimer()">Toggle Timer</button>
@if (timerIsActive()) {
<app-my-timer></app-my-timer>
}
// takeuntil-example.ts
import { Component, signal } from '@angular/core';
import { MyTimer } from './my-timer/my-timer';
@Component({
selector: 'app-takeuntil-example',
imports: [MyTimer],
templateUrl: './takeuntil-example.html',
styleUrl: './takeuntil-example.scss'
})
export class TakeuntilExample {
public timerIsActive = signal(false);
public showTimer(): void {
this.timerIsActive.set(true);
}
public hideTimer(): void {
this.timerIsActive.set(false);
}
}
The Problematic component
This is the child component where we’ll demonstrate the memory leak issue. It has both a long-running observable (timer) and a finite observable (API call):
<!-- my-timer.html -->
<p>The timer: {{ secondsRunning() }}</p>
<p>The dogs: {{ dogs() | json }}</p>
// my-timer.ts
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { DataService } from '../data.service';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-my-timer',
imports: [JsonPipe],
templateUrl: './my-timer.html',
styleUrl: './my-timer.scss'
})
export class MyTimer implements OnInit, OnDestroy {
private dataService = inject(DataService);
public dogs = signal<unknown>(null);
public secondsRunning = signal(0);
private subscription = new Subscription();
private timer = timer(0, 1000);
constructor() {
// todo
}
public ngOnInit(): void {
// todo
}
public ngOnDestroy(): void {
console.log('on Destroy');
}
}
2. Memory Leak Setup
We have the setup that is as of now producing memory leaks. Loading Data from a Service never was a problem. Those observables are automatically completing after emitting a response. However we will still load data even if you close the component before receiving an answer.
// my-timer.ts
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { DataService } from '../data.service';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-my-timer',
imports: [JsonPipe],
templateUrl: './my-timer.html',
styleUrl: './my-timer.scss'
})
export class MyTimer implements OnInit, OnDestroy {
private dataService = inject(DataService);
public dogs = signal<unknown>(null);
public secondsRunning = signal(0);
private subscription = new Subscription();
private timer = timer(0, 1000);
constructor() {
// this will leak
this.timer.pipe(tap((value) => console.log(value))).subscribe({
next: (value: number) => {
this.secondsRunning.set(value);
}
});
}
public ngOnInit(): void {
// this is fine. it's a automatically completing observable after 1 answer
this.dataService.getDogs().subscribe({
next: (data: unknown) => {
console.log('Dogs received:', data);
this.dogs.set(data);
}
});
}
public ngOnDestroy(): void {
console.log('on Destroy');
}
}
The console output
This is the console output. You can see that the timer is now repeating multiple times
0 my-timer.ts:24
Dogs received: (2) [{…}, {…}] my-timer.ts:35
1 my-timer.ts:24
2 my-timer.ts:24
on Destroy my-timer.ts:42
# here the component was destroyed. Notice that the subscription is still running
3 my-timer.ts:24
4 my-timer.ts:24
# here we created the component again. Notice we have 2 subscriptions now
0 my-timer.ts:24
5 my-timer.ts:24
# The Service call is there only once
Dogs received: (2) [{…}, {…}] my-timer.ts:35
1 my-timer.ts:24
6 my-timer.ts:24
2 my-timer.ts:24
7 my-timer.ts:24
on Destroy my-timer.ts:42
3 my-timer.ts:24
8 my-timer.ts:24
4 my-timer.ts:24
9 my-timer.ts:24
5 my-timer.ts:24
3. Classic way
The classic solution has been the manual approach: collect all subscriptions and clean them up in the ngOnDestroy lifecycle hook. This was the standard pattern before RxJS operators like takeUntil became available.
It is Explicit and easy to understand and works with any subscription. However there is some Boilerplate code and it is easy to forget to add new subscriptions to the collection
// my-timer.ts
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { DataService } from '../data.service';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-my-timer',
imports: [JsonPipe],
templateUrl: './my-timer.html',
styleUrl: './my-timer.scss'
})
export class MyTimer implements OnInit, OnDestroy {
private dataService = inject(DataService);
public dogs = signal<unknown>(null);
public secondsRunning = signal(0);
private subscription = new Subscription();
private timer = timer(0, 1000);
constructor() {
this.subscription.add(
this.timer.pipe(tap((value) => console.log(value))).subscribe({
next: (value: number) => {
this.secondsRunning.set(value);
}
})
);
}
public ngOnInit(): void {
this.dataService.getDogs().subscribe({
next: (data: unknown) => {
console.log('Dogs received:', data);
this.dogs.set(data);
}
});
}
public ngOnDestroy(): void {
console.log('on Destroy');
this.subscription.unsubscribe();
}
}
The console output
This is the console output. We fixed the memory leak! But now we have to track the active subscriptions.
0 my-timer.ts:24
Dogs received: (2) [{…}, {…}] my-timer.ts:35
1 my-timer.ts:24
2 my-timer.ts:24
on Destroy my-timer.ts:42
0 my-timer.ts:24
Dogs received: (2) [{…}, {…}] my-timer.ts:35
1 my-timer.ts:24
2 my-timer.ts:24
on Destroy my-timer.ts:42
4. Modern way
This approach uses the takeUntil operator from RxJS, which is much cleaner than the classic method. With takeUntil, you don’t need to manually track subscriptions. Instead, you pipe all observables through a “destroy” subject that signals when to unsubscribe.
How it works:
- Create a
Subjectcalleddestroy$in your component - Add
takeUntil(this.destroy$)to every observable’s pipe chain - In
ngOnDestroy, emit a value and complete the subject - All piped observables automatically unsubscribe
Pros:
- No need for a
Subscriptionobject - Cleaner than the classic approach
- Can cancel API requests if the component is destroyed before they complete
- Works with services too
Cons:
- Still need to add
takeUntilto every subscription - Still need to implement
ngOnDestroy
Reference: https://www.learnrxjs.io/learn-rxjs/operators/filtering/takeuntil
// my-timer.ts
import { Component, inject, OnInit, signal } from '@angular/core';
import { Subject, takeUntil, tap, timer } from 'rxjs';
import { DataService } from '../data.service';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-my-timer',
imports: [JsonPipe],
templateUrl: './my-timer.html',
styleUrl: './my-timer.scss'
})
export class MyTimer implements OnInit, OnDestroy {
export class MyTimer implements OnInit {
private dataService = inject(DataService);
public dogs = signal<unknown>(null);
public secondsRunning = signal(0);
private subscription = new Subscription();
private destroy$ = new Subject<void>();
private timer = timer(0, 1000);
constructor() {
this.timer
.pipe(
takeUntil(this.destroy$),
tap((value) => console.log(value))
)
.subscribe({
next: (value: number) => {
this.secondsRunning.set(value);
}
});
}
public ngOnInit(): void {
this.dataService
.getDogs()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (data: unknown) => {
console.log('Dogs received:', data);
this.dogs.set(data);
}
});
}
public ngOnDestroy(): void {
console.log('on Destroy');
this.destroy$.next();
this.destroy$.complete();
}
}
The console output
This is the console output.
0 my-timer.ts:28
1 my-timer.ts:28
2 my-timer.ts:28
Dogs received: (2) [{…}, {…}] my-timer.ts:43
3 my-timer.ts:28
on Destroy my-timer.ts:50
0 my-timer.ts:28
1 my-timer.ts:28
2 my-timer.ts:28
Dogs received: (2) [{…}, {…}] my-timer.ts:43
3 my-timer.ts:28
on Destroy my-timer.ts:50
0 my-timer.ts:28
1 my-timer.ts:28
on Destroy my-timer.ts:50
# Notice that we did not get the Dogs response after destroying the component.
Here we see that the request is properly canceled

5. State of the art
Angular now provides the takeUntilDestroyed operator (since Angular 16), which is the best approach. It’s part of the @angular/core/rxjs-interop package and automatically unsubscribes when the component is destroyed. We can skip the ngOnDestroy entirely!
How it works:
takeUntilDestroyed()listens to theDestroyRefof the current injection context- When the component is destroyed, the
DestroyRefemits and automatically completes all piped observables - Works in injection contexts (components, services, directives, etc.)
- Can pass a custom
DestroyRefif you need to tie subscriptions to a different lifecycle
This is the cleanest and most modern approach. There is no manual ngOnDestroy lifecycle hook needed. It’s automatically integrated with Angular’s dependency injection and works in services and anywhere with injection context
Reference: https://angular.dev/ecosystem/rxjs-interop/take-until-destroyed
import { Component, inject, OnInit, signal } from '@angular/core';
import { tap, timer } from 'rxjs';
import { DataService } from '../data.service';
import { JsonPipe } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-my-timer',
imports: [JsonPipe],
templateUrl: './my-timer.html',
styleUrl: './my-timer.scss'
})
export class MyTimer implements OnInit {
private dataService = inject(DataService);
// this is optional and only needed outside of injection context
private destroyRef = inject(DestroyRef);
public dogs = signal<unknown>(null);
public secondsRunning = signal(0);
private destroy$ = new Subject<void>();
private timer = timer(0, 1000);
constructor() {
this.timer
.pipe(takeUntil(this.destroy$))
.pipe(takeUntilDestroyed()) // in an injection context we don't even need that
.pipe(takeUntilDestroyed(this.destroyRef)
tap((value) => console.log(value))
)
.subscribe({
next: (value: number) => {
this.secondsRunning.set(value);
}
});
}
public ngOnInit(): void {
this.dataService
.getDogs()
.pipe(takeUntil(this.destroy$))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (data: unknown) => {
console.log('Dogs received:', data);
this.dogs.set(data);
}
});
}
public ngOnDestroy(): void {
console.log('on Destroy');
this.destroy$.next();
this.destroy$.complete();
}
}
The console output
0 my-timer.ts:30
1 my-timer.ts:30
2 my-timer.ts:30
Dogs received: (2) [{…}, {…}] my-timer.ts:46
3 my-timer.ts:30
4 my-timer.ts:30
# component killed and restarted later
0 my-timer.ts:30
1 my-timer.ts:30
2 my-timer.ts:30
Dogs received: (2) [{…}, {…}] my-timer.ts:46
3 my-timer.ts:30
# component killed and restarted later
0 my-timer.ts:30
1 my-timer.ts:30
2 my-timer.ts:30
# component killed. No Dogs log
cleaned up code
this is just the cleaned up code convenient to read
import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
import { tap, timer } from 'rxjs';
import { DataService } from '../data.service';
import { JsonPipe } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-my-timer',
imports: [JsonPipe],
templateUrl: './my-timer.html',
styleUrl: './my-timer.scss'
})
export class MyTimer implements OnInit {
private dataService = inject(DataService);
private destroyRef = inject(DestroyRef);
public dogs = signal<unknown>(null);
public secondsRunning = signal(0);
private timer = timer(0, 1000);
constructor() {
this.timer
.pipe(
takeUntilDestroyed(this.destroyRef),
tap((value) => console.log(value))
)
.subscribe({
next: (value: number) => {
this.secondsRunning.set(value);
}
});
}
public ngOnInit(): void {
this.dataService
.getDogs()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (data: unknown) => {
console.log('Dogs received:', data);
this.dogs.set(data);
}
});
}
}
A Few Things To Keep In Mind
takeUntilDestroyedis an operator, so it needs to be part of apipe. You cannot use it standalone.- It only works with observables. If you are using
asyncpipes in your template, you do not need it at all because theasyncpipe unsubscribes automatically. - If you are using
switchMapor other higher-order operators,takeUntilDestroyedwill still work as expected because it completes the outer observable.
Summary
takeUntilDestroyed is a small but very useful operator that removes a lot of boilerplate from your Angular components. If you are still using the Subject + takeUntil pattern, it is worth switching to this operator. It is cleaner, less error-prone, and it works in services too.