Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building a Generic Observable Class

Recommended resources

The freely available resources this lesson will direct you to.

Hello! Welcome back to our project.

In our previous lessons, we built two fundamental components of our reactive library: the Observer interface, which defines the "consumer" of our data stream, and the Subscription class, which provides the "handle" to manage the lifecycle of a data stream execution.

Today, we will implement the central piece that connects them: the Observable class. This class is the source of all values, the object that a consumer subscribes to.

This lesson directly addresses the learning outcome: Implement the core Observable class using a generic type parameter for the emitted value. We will define its structure and, most importantly, its constructor, which embodies the core principle of how observables work.

1. The Core Design: A "Recipe" for Producing Values

Before we write any code, let's consider the core design. What is an Observable? Is it just a container for data, like an array?

While an observable can represent a static collection of data, its true power lies in its ability to represent any stream of values over time—synchronous or asynchronous. To achieve this flexibility, we won't design our Observable to hold data directly. Instead, we will design it to hold a function.

This function, which we'll call the producer function (or sometimes the subscriber function), is a "recipe" that knows how to produce values. When someone subscribes to the Observable, we will execute this recipe.

The producer function will have one crucial responsibility: given an Observer, it must start generating values and push them to the observer by calling observer.next(), observer.error(), or observer.complete().

This design is incredibly powerful. The producer function can be anything:

  • A loop that iterates over an array.
  • A setTimeout or setInterval call.
  • A DOM event listener.
  • A WebSocket message handler.
  • A wrapper around a Promise's resolution.

By encapsulating this logic within a function that the Observable holds, we create a single, unified API for an infinite variety of data sources.

2. The Observable Class Structure

To understand this producer-based design in detail, let's study a well-explained implementation. The following article provides the complete code for a basic Observable and then breaks down its core mechanism.

Demystifying RxJS, Part I: Building our own Observables

The article 'Demystifying RxJS, Part I' by Travis Kaufman offers a clear and concise implementation of an Observable that we can use as our foundation.

First, review the complete Observable class code presented at the beginning of the article (it's a single code block). Then, read the section titled 'Observables' which explains the constructor and the role of what the author calls the observe() factory function (our producer function). Focus on how this function 'links the Observer to its underlying stream' and can return a teardown function.

As the article explains, the key is the function passed to the constructor. It defines the observable's behavior. It also highlights a critical feature we planned for in our last lesson: the producer function can return a teardown function. This is the logic that our Subscription object will execute upon unsubscription.

3. Implementing the Observable Class

Now, let's apply these concepts and create our Observable class.

Your Task: Create and Implement the Observable Class

  1. In your src directory, create a new file named Observable.ts.
  2. Add the following code to the file. This implementation is based on the article we just read, adapted to use our existing Observer and Subscription types.
// src/Observable.ts

import { Observer } from "./Observer";
import { Subscription } from "./Subscription";

/**
 * A function that is called when the Observable is initially subscribed to. This
 * function is given an Observer, to which new values can be `next`ed, or an
 * `error` method can be called to raise an error, or `complete` can be called
 * to notify of a successful completion.
 *
 * It can optionally return a teardown function that will be executed upon
 * unsubscription.
 */
export type Producer<T> = (
  observer: Observer<T>
) => (() => void) | void;

/**
 * Represents a push-based collection of values over time.
 */
export class Observable<T> {
  /**
   * The producer function that defines the observable's behavior.
   * @private
   */
  private _producer: Producer<T>;

  /**
   * @param producer A function that, when subscribed to, is invoked with an
   * Observer as an argument.
   */
  constructor(producer: Producer<T>) {
    this._producer = producer;
  }

  /**
   * Subscribes to the stream and provides a handler for notifications.
   *
   * We will implement the full logic for this method in the next lesson.
   * For now, it serves as a placeholder to complete the class structure.
   */
  subscribe(observer: Observer<T>): Subscription {
    // The actual implementation will be the focus of our next lesson.
    // It will involve:
    // 1. Creating a Subscription object.
    // 2. Calling this._producer(observer).
    // 3. Capturing the returned teardown logic and giving it to the Subscription.
    // 4. Returning the Subscription.
    
    // For now, we return an empty subscription.
    return new Subscription();
  }
}

Let's break this down:

  • Producer<T>: We've created a type alias for our producer function. This makes the code more readable. It clearly states that the function takes an Observer<T> and can return a teardown function (() => void) or nothing (void).
  • Observable<T>: The class is generic. The type parameter T represents the type of value that will be emitted by this observable. This ensures type safety throughout our library. For example, an Observable<number> can only have observers that expect numbers.
  • private _producer: Producer<T>: A private property to store the "recipe" function that was passed to the constructor.
  • constructor(producer: Producer<T>): The constructor simply takes the producer function and stores it. The Observable instance itself is immutable; its behavior is defined at creation.
  • subscribe(...): This is the method that consumers will call. As noted in the code comments, we've left its implementation minimal for now. In our next lesson, we will fully implement it to bring the Observer, Producer, and Subscription together.

4. An Alternative Perspective

The producer-function approach is the standard for libraries like RxJS because of its flexibility. However, it's insightful to consider alternative designs. Another way to think about an observable is as a simple collection of values.

The following resource explores a different initial implementation.

How I created an Observable library - part 1

For a different perspective on an Observable's internal structure, let's briefly look at another implementation from the article 'How I created an Observable library - part 1'.

Find the section titled 'The Observable class', located under the 'The first implementation' subsection. In the body text below the heading, read the Observable class implementation. Notice how this implementation's constructor takes ...initialSequence: T[] and stores it in a private array _innerSequence. Contrast this with the producer function approach we've just implemented.

This alternative constructor(...initialSequence: T[]) is simpler and is essentially what a factory function like of(1, 2, 3) will do internally. However, it can only represent a synchronous, finite stream of known values. It cannot, by itself, represent DOM events or values from a setInterval.

Our producer-based constructor(producer: Producer<T>) is more fundamental because it can model both cases. We can easily write a producer that iterates over an array, and we can also write one that wraps an asynchronous API. This general-purpose design is why it's the foundation of modern reactive libraries.

Conclusion

In this lesson, we have defined the core structure of our Observable class. We've established the most important design principle: an observable encapsulates a producer function that defines how to generate notifications for an observer.

Key Takeaways:

  • The Observable<T> class is generic to provide type safety for the values it emits.
  • The core of an Observable is its producer function, a "recipe" for generating values, which is passed into the constructor.
  • This design is highly flexible, allowing observables to represent any synchronous or asynchronous data source with a unified interface.
  • The producer function can return a teardown function, which will be used by the Subscription to clean up resources.

Preview of the Next Lesson:

We now have all three core components: Observer, Subscription, and the Observable class structure. In the next lesson, we will complete the puzzle by implementing the subscribe method. We'll see how it executes the producer function, connects it to the observer, and uses the Subscription class to manage the entire lifecycle, including the crucial teardown logic.

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

Sign up