Create your own
Lesson illustration

Implementing Subscription Teardown

Hello! Let's continue building our reactive library.

In our last lesson, we defined the Observer interface, establishing the "contract" for how a consumer receives values, errors, and completion signals from a data stream. We now have the "what" for data consumption. Today, we'll focus on managing the connection itself.

This lesson addresses the learning outcome: Implement a Subscription class with teardown logic for resource management. A Subscription acts as a handle to an active observation. Its most critical role is to provide a way to unsubscribe, allowing us to clean up resources and prevent common issues like memory leaks. This is a fundamental concept for building robust asynchronous applications.

1. The "Why": Preventing Memory Leaks

In front-end development, you've likely encountered situations where you add an event listener but forget to remove it. If the element is removed from the DOM, the listener can keep a reference to it, preventing garbage collection. This is a classic memory leak.

Observables that represent long-lived or infinite event sources (like DOM events, WebSockets, or timers) present the same risk. If we subscribe to an observable but never signal that we're no longer interested, the subscription might live on in memory indefinitely.

To see a practical demonstration of this problem and its solution, let's watch a segment from the Academind tutorial we looked at previously.

OBSERVABLES, OBSERVERS & SUBSCRIPTIONS | RxJS TUTORIAL

This part of the "OBSERVABLES, OBSERVERS & SUBSCRIPTIONS" video clearly explains why unsubscribing is crucial for infinite observables and demonstrates how calling unsubscribe() stops the flow of data.

About 15 minutes into the video, watch memory leak fix. Focus on the concept of a memory leak in the context of an infinite observable and see how the unsubscribe method on the subscription object solves this.

This illustrates the core purpose of a Subscription: it represents an active execution and gives us the power to terminate it.

2. The "What": Subscription and Teardown Logic

Now that we understand the "why," let's formalize the "what." What exactly is a Subscription, and how does it manage cleanup?

A Subscription is an object that represents a disposable resource, usually the execution of an Observable. It has one primary method, unsubscribe(), which is called to release resources and cancel the execution.

When unsubscribe() is called, it needs to execute some cleanup code. This is often called teardown logic. This logic is responsible for undoing whatever setup was done when the subscription started. For example:

  • If the observable wrapped setInterval, the teardown logic would be clearInterval.
  • If it wrapped addEventListener, the teardown logic would be removeEventListener.

The following article provides an excellent, in-depth explanation of the Subscription lifecycle and the role of teardown logic.

To unsubscribe, or not to unsubscribe, that is the question.

The article "To unsubscribe, or not to unsubscribe, that is the question" by Jurek Wozniak is a great resource. We'll read the sections that define a Subscription and explain what happens when it ends.

Please read the following three sections. In the 'What is a Subscription?' section, which comes after the opening discussion on what happens if you don't unsubscribe, read the Subscription definition. Next, in 'How can a Subscription end?', read ways Subscriptions end. Finally, in 'What happens after the Subscription ends?', read teardown logic. Focus on the definition of a Subscription, the three ways it can end (complete, error, unsubscribe), and the concept of 'Finalizer/Teardown logic'.

The key takeaway is that a subscription can end in three ways, but regardless of how it ends (naturally via complete/error or manually via unsubscribe), the same teardown logic should be executed to ensure resources are always released. Our Subscription class will be the mechanism that guarantees this.

3. The "How": Implementing the Subscription Class

We're now ready to implement the Subscription class. It needs to do two main things:

  1. Store the teardown logic that it's responsible for.
  2. Provide an unsubscribe() method that executes this logic and ensures it only runs once.

Let's start by creating the file and the basic class structure.

Your Task: Create and Implement the Subscription Class

  1. In your project's src directory, create a new file named Subscription.ts.
  2. Add the following code to the file.
// src/Subscription.ts

/**
 * Represents the execution of an Observable. A Subscription has one important
 * method, `unsubscribe`, that takes no argument and just disposes of the
 * resource held by the subscription.
 */
export class Subscription {
  /**
   * A flag to indicate whether this Subscription has already been unsubscribed.
   * @private
   */
  private _closed = false;

  /**
   * The teardown logic to be executed when the subscription is unsubscribed.
   * This is a function that will be called to clean up resources.
   * @private
   */
  private _teardown: (() => void) | null;

  /**
   * @param teardown A function to call when the subscription is unsubscribed.
   */
  constructor(teardown?: () => void) {
    this._teardown = teardown || null;
  }

  /**
   * A getter to check if the subscription is closed.
   * @returns `true` if the subscription is closed, `false` otherwise.
   */
  get closed(): boolean {
    return this._closed;
  }

  /**
   * Disposes the resources held by the subscription by executing the
   * teardown logic. If the subscription is already closed, this method
   * does nothing.
   */
  unsubscribe(): void {
    if (this._closed) {
      return; // Do nothing if already unsubscribed
    }

    this._closed = true;

    // Execute the teardown logic if it exists
    if (this._teardown) {
      this._teardown();
    }
  }
}

Let's break down this implementation:

  • _closed: A private boolean flag that prevents the teardown logic from being executed multiple times. The unsubscribe() method is idempotent.
  • _teardown: A private property to hold the cleanup function. We make it optional in the constructor, as some observables might not need any cleanup (e.g., an observable that synchronously emits a few values and completes).
  • constructor(teardown?: () => void): It accepts an optional teardown function and stores it.
  • closed getter: A public way to check the status of the subscription without allowing external code to modify it.
  • unsubscribe(): This is the public API. It checks the _closed flag, and if the subscription is active, it sets the flag to true and then executes the stored _teardown function.

4. Connecting Subscription to the Observable

We've built the Subscription class, but where does the teardown function passed to its constructor come from?

This is the responsibility of the producer function inside the Observable. When we implement our Observable class in the next lesson, its constructor will accept a function (the producer). This producer function receives the Observer as an argument and is responsible for generating values. Crucially, this producer can return a function, and that returned function is our teardown logic.

The Observable.subscribe() method will then:

  1. Execute the producer function.
  2. Capture the returned teardown logic.
  3. Create a new Subscription instance, passing it that teardown logic.
  4. Return the new Subscription to the caller.

Let's look at a conceptual example of this flow.

Demystifying RxJS, Part I: Building our own Observables

The article "Demystifying RxJS, Part I" provides a clear explanation of how the teardown function is returned from the observable's core logic.

In the 'Observables' section, read the two paragraphs that start with 'If there is any cleanup logic...': teardown functions. Then, in the subsection 'Solidifying our understanding: Observable factories', look at the code example for fromEvent(), which is introduced in the paragraph beginning "Finally, let's implement fromEvent()": the fromEvent implementation. Notice how it returns a function that calls removeEventListener. This is a perfect example of teardown logic.

This pattern creates a clean separation of concerns:

  • The producer knows how to set up and tear down its specific resources.
  • The Subscription class knows how to manage the lifecycle of the teardown.
  • The consumer of the observable simply calls unsubscribe() without needing to know any of the implementation details.

Conclusion

In this lesson, we have built a crucial piece of our reactive library's infrastructure. The Subscription class provides the resource management capabilities necessary for a robust and leak-free system.

Key Takeaways:

  • A Subscription represents an active, ongoing execution of an Observable.
  • Calling the unsubscribe() method is the primary way to manually terminate a subscription and clean up resources.
  • Teardown logic is the cleanup code (e.g., clearInterval, removeEventListener) that is executed upon unsubscription.
  • Our Subscription class encapsulates this behavior by storing a teardown function and executing it once when unsubscribe() is called.
  • The teardown logic itself is provided by the Observable's internal producer function, creating a clean contract.

Preview of the Next Lesson:

We now have the Observer (the what) and the Subscription (the how). In our next lesson, we will finally bring these two pieces together by implementing the central Observable class. We will implement its subscribe method, which will connect an Observer to a producer function and return the Subscription that manages the lifecycle of that connection.

Can't find a good explanation? Sign up and we'll make it for you

Sign up