651.288.7000 info@intertech.com

Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.

Overview

Angular is heavily reliant on the RxJS library which is a library that implements Reactive programming in JavaScript. Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change (Wikipedia). The core concept in the RxJS library is the Observable. Observables are streams of data used to inform various parts of the application of a change that may be relevant to that functionality. This article discusses some best practices, some opinionated ideas, and pitfalls to be aware of that are not always immediately obvious when working with Observables.

To follow along with the articles there a two repositories created: a starting point and final solution.

Review

Let’s start with some review on Observables.  If anything isn’t familiar, I suggest reviewing the Angular Observables documentation for a more thorough overview on the basics of Observables.  The common use pattern for an Observable is to provide updates as changes happen elsewhere in the application. A prime example of this would be the current user of the application.  A user / authentication service can provide a user Observable that will emit a new value if the user logs out or a new user logs in.  Any part of the application dependent on user settings can react to the change and make the necessary updates.  As a user change could happen through a modal window, it’s a prime example of a change that can occur without any routing or other actions to trigger a change.  Below is a basic implementation of this concept.

@Component({
    selector: 'app-greeting',
    template: '<div>Hello, {{user.name}}</div>'
})
export class GreetingComponent implements OnInit {
    public user;

    constructor(private userService: UserService) {}

    ngOnInit(): void {
        this.userService.currentUser().subscribe(newUser => this.user = newUser);
    }
}

Clean Up After Yourself

 

 Observables have a potential problem.  Let’s take the example above, what happens when you route to a new page and create a new component?  Every time a user logs in, the new user information is still stored and processed by the GreetingComponent because the subscription is still active.  Now what happens if it’s something with a much larger data set, or more frequent updates?  This is a memory leak that can cause bugs and crashes that are very difficult to trace and predict. 

 

 Demonstrating a Leak

We can demonstrate the issue of a memory leak with a few changes to the example above.  First, we can create a dummy service that emits a continual stream of data by emitting a new random name every two seconds:

export interface User {
    name: string;
}
const names: string[] = [
    'Charlie Brown',
    'Darth Vader',
    SNIP...
];

@Injectable({
    providedIn: 'root'
})
export class UserService {
    currentUser(): Observable<User> {
        return interval(2000).pipe(map(this.getRandomUser));
    }

    getRandomUser(): User {
        const index = Math.floor(Math.random() * names.length);
        return {
            name: names[ index ]
        };
    }
}
To see the issue we can add a log statement every time the data changes in the greeting component:

ngOnInit(): void {
    this.userService.currentUser().subscribe(newUser => {
        this.user = newUser;
        console.log(newUser);
    });
}

We can see the data updates in the view and we can see a logged message in the console for each historical value. This is exactly what we would expect, but what happens when we navigate away from the component?  When navigating elsewhere you can see the console messages keep rolling in!

 

What is even worse, is if we return to the greeting component it won’t grab the existing stream, it creates a new subscription.  Navigate away and back a few times and you can see the data rolling in at a high speed!

 

Avoiding Leaks

To avoid a memory leak, we can unsubscribe the the Observable and stop processing any updates.  There are many articles and methodologies for handling this that you can review to accomplish this, however it can add a fair amount of similar code to every subscription.  There are a few things to keep in mind that can reduce the boilerplate in handling Observables:

    • Limit subscriptions – see 1- Smart vs. Dumb Components.  By structuring you app into smart and dumb components, you can reduce the number of subscriptions and convert them to Inputs.  If only your high level controller components need to subscribe, then the issue is drastically reduced.
    • Use the Async Pipe – the Angular async pipe can be used to subscribe to a promise from the view.  The async pipe handles subscriptions and will unsubscribe for you, so if you only need data in the view then you don’t need to subscribe in the component.
    • Use a consistent method – create a reusable component that handles most of the work for you so you don’t need to write the same boilerplate every time.  Create a base class that provides a core that can be used to unsubscribe and provides an Observable that fires and completes in the OnDestroy hook that can be used to unsubscribe when leaving a component.

In order to fix the memory leak, we can implement logic to unsubscribe when destroying a component.  Using the method described in the linked post (without using an abstract class to allow for more clarity), we can create our own short lived Observable that triggers an unsubscribe using the takeUntil method:

export class GreetingComponent implements OnInit, OnDestroy {
    public user;
    public unsubscribe = new Subject<any>();

    constructor(private userService: UserService) { }

    ngOnInit(): void {
        this.userService.currentUser()
            .pipe(takeUntil(this.unsubscribe))
            .subscribe(newUser => {
                this.user = newUser;
                console.log(newUser);
            });
    }

    ngOnDestroy(): void {
        this.unsubscribe.next();
        this.unsubscribe.complete();
    }
}
Now when testing, the logging stops when navigating away and picks back up when we go back to the component.  

A Problem with Observables

NOTE: This section is completely my opinion, you can find many discussions that argue against this practice with valid points which I will mention, but this has been my findings with my experience using Angular.  The most important concept is to use consistency in an application to semantically communicate expected functionality to improve the quality of code.  

Observables represent a continual stream of information.  The have many benefits, including delayed execution – if nobody is subscribed, the action isn’t fired. But let’s take a look at an example that is slightly modified from the one above:      

@Component({
    selector: 'app-greeting',
    template: '<div>Hello, {{user.name}}</div>'
})
export class GreetingComponent implements OnInit {
    public user;

    constructor(private userService: UserService) {}

    ngOnInit(): void {
        this.userService.currentUser().subscribe(newUser => this.user = newUser);
        this.userService.getUserPreferences().subscribe(prefs => console.log(prefs));
    }
}
If you look at the code above, you would expect the component to get new user information, and new user preferences when a new user logs in. However, when running this code you find that the greeting changes, but you never see the new user preferences loaded. You gnash your teeth and bang your head on the desk looking for the mistake until you finally look at the UserService:
@Injectable()
export class UserService {
    private _user = new BehaivorSubject(null);

    constructor(private authService: AuthService, private http: HttpClient) {}

    currentUser(): Observable<User> {
        return this._user.asObservable();
    }

    login(credentials) {
        if (this.authService.validate(credentials)) {
            this._user.next(credentials.userName);
        }
    }

    getUserPreferences(): Observable<UserPreference> {
        return this.http.get(`/api/userPreferences/${this._user.getValue()}`);
    }
}
While this example is a bit contrived and would be more in depth, the point remains the same. One observable is updated continually with new information, but one is a single execution http request. There is nothing about the service API that helps communicate this, so you are left with the need to view the underlying implementation to understand the results. So the question becomes, can we fix this to help communicate the functionality to any consumer of the service? The culprit here is the getUserPreferences method, let’s change it:
getUserPreferences(): Promise<UserPreference> {
    return this.http.get(/api/userPreferences/`${this._user.getValue()}`).toPromise();
}

NOTE: The toPromise method is being deprecated in future versions of RxJS in lieu of the lastValueFrom function

This is a small change with a big impact.  It clearly helps communicate this is a single value return.  By maintaining this convention of single values use Promises, multiple value streams use Observables we can greatly improve the understanding of underlying implementation with very little loss of functionality or additional code.  There certainly are other improvements that can be made, such as a better name, using a parameter instead of the current user value, etc. that could also help, but the use of a Promise is the single most effective communication tool on the intent of this method.

It now becomes clear how this can and should be consumed in the component:

@Component({
    selector: 'app-greeting',
    template: '<div>Hello, {{user.name}}</div>'
})
export class GreetingComponent implements OnInit {
    public user;

    constructor(private userService: UserService) {}

    ngOnInit(): void {
        this.userService.currentUser().subscribe(newUser => {
            this.user = newUser;
            this.userService.getUserPreferences().then(prefs => console.log(prefs));
        });
    }
}

We know that the Promise is a single return and we want to get that information whenever a user changes, so we can call the method when we need the new information.

We also get a side benefit from this.  As mentioned in the previous section, we need to unsubscribe from Observables.  This isn’t the case for http calls from HttpClient, but how is any consumer of a service supposed to know that?  This convention helps to clearly lay out when Observables need to be unsubscribed and also reduces the need of extraneous unsubscribes when they aren’t necessary.

The downside and counter argument to this approach is Promises have less functionality than Observables, specifically in regards to the pipable functions, and I will agree with that point.  There is certainly an argument for consistently using Observables in an application for this reason and also to reduce the need to learn an addtional API.  Ultimately the decision depends on the application needs, but one thing is true no matter the approach – find a way that works and stick to it consistently.  An application that has a reliable method of communicating functionality – be it naming convention, inference by type, or comments – is a far easier application to understand and maintain.

Conclusion

Observables are a powerful tool that greatly helps Angular applications be reactive and clean – however there are some potential pitfalls with their usage that developers should consider.

Related Articles In This Series

Coming next week….Angular Development #7 – Provider Scopes – Component Level Services

 

About Intertech

Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.