RxJS Mastery – #4 Subscription

Once you subscribe to an Observable a RxJS Subscription is returned and created. Let’s see what we can do with that.

RxJS Subscription explained 🎓

A subscription usually represents an execution of an Observable. It is the access to the disposable resource or async task. The subscription is returned from the subscribe() method call.

What problems do RxJS Subscriptions solve? 🚧

The main purpose of a subscription is to allow unsubscribing. The unsubscribe() method on a subscription invokes the Observable’s teardown logic. This cleans up and frees up resources. Let’s take an example with the interval function. This creation operator creates an Observable that emits sequential numbers every specified interval of time (docs).

import { interval } from 'rxjs';

const observable = interval(1000);
const subscription = observable.subscribe(x => console.log(x));

subscription.unsubscribe();

The unsubscribe in above code will stop the interval. Interestingly, you can also put together subscriptions:

subscription.add(childSubscription);
subscription.unsubscribe();

When calling unsubscribe() on the “parent” subscription also the unsubscribe() of the child or children is triggered. This is a pattern that you sometimes see to handle the clean up of multiple subscriptions.

What happens if you fail to unsubscribe?

The async task behind the Observable might continue to run although it’s not needed anymore. This is consuming more resources. Furthermore, if an Observable involves references to other parts of the code, garbage collection could not be able to do its job.

How to test Subscriptions 🚦

Consulting the RxJS marbles documentation we learn the following about the marbles diagram for testing:

  • '^' subscription point: shows the point in time at which a subscription happens.
  • '!' unsubscription point: shows the point in time at which a subscription is unsubscribed.

In below code we have a hot Observable emitting a, b, c, and d at certain timeframes. With sub we define a subscription and indicate with ^ when we subscribe and with ! when we unsubscribe. This allows us test for the output and to see if the subscription really happened through expectSubscriptions.

const testScheduler = new TestScheduler((actual, expected) => {
    expect(actual).toEqual(expected);
});

describe('marbles testing', () => {
    it('should test the subscription', () => {
        testScheduler.run((helpers) => {
            const { hot, expectSubscriptions, expectObservable } = helpers;

            const source = hot('--a-b-c-d-');
            const sub = '       ---^---!--';
            const output = '    ----b-c---';

            expectObservable(source, sub).toBe(output);
            expectSubscriptions(source.subscriptions).toBe([sub]);
        });
    });
});

Let’s see how we can use this in a “real” example. To demonstrate it, we are using the take() operator. This operator only “reads” a certain amount of values and then automatically unsubscribes. In below test we are having a service that emits the values a-d in a certain time. For that we are using a hot observable.

it('should test the subscription dependent on take', () => {

        testScheduler.run((helpers) => {
            const { expectSubscriptions, hot } = helpers;
            let myService = {
                data: () => hot('-a-b-c-d', { a: 1, b: 2, c: 3, d: 4}),
            };

            const observable = myService.data();
            observable.pipe(take(3)).subscribe();
            const sub = '^----!';

            expectSubscriptions(observable.subscriptions).toBe([sub]);
        });
    });

Using expectSubscriptions we can show that the subscription happens at time frame 0. More interestingly we can also demonstrate that the subscription ends at time frame 5 (denoted by !). At this time the observable has read the 3 values specified by take(3).

Exercise for Subscription 💪

Test if in our last example also an unsubscribe happens if we take the first 5 values. If so, why? The code can also be found on GitHub.