RxJS Tip: Understand the Terminology: Observer

Deborah Kurata - Nov 16 '20 - - Dev Community

We've discussed Observable and Subscription. Another key RxJS concept is Observer.

What Is an Observer?

An Observer watches for emissions and notifications from an Observable after a consumer subscribes to that Observable.

An Observer defines an interface with callback functions for each type of Observable notification: next, error, and complete.

Use the next callback to process the emitted item.
Use the error callback to implement exception handling.
Use the complete callback to perform cleanup when the Observable is complete. (This is not used often in an Angular application.)

How Do You Define an Observer?

There are several ways to define an Observer.

Explicitly (Uncommon)

Though not common, you can define an Observer explicitly by creating an object with three callback functions: next, error, and complete.

// Define an explicit observer (uncommon)
const observer = {
  next: apple => console.log(`Apple was emitted ${apple}`),
  error: err => console.log(`Error occurred: ${err}`),
  complete: () => console.log(`No more apples, go home`)
};
Enter fullscreen mode Exit fullscreen mode

The next method argument is the emitted item. The arrow function specifies what to do with that item. In this case, we are simply logging it to the console.

The error method argument is the error emitted when an error occurs. The arrow function specifies what to do with the error notification. In this case, we are logging the error to the console.

The complete method has no argument. The arrow function defines what to do when the Observable is complete. In this case, it logs a message to the console.

We then pass that Observer object into the Observable subscribe method to react to the Observable's emissions and notifications.

// Pass the Observer into the subscribe (uncommon)
const sub = source$.subscribe(observer);
Enter fullscreen mode Exit fullscreen mode

Pass a Single Callback

It is more common to pass the Observer callback functions directly into the Observable subscribe method.

You can only pass one object to the subscribe method.

If you only need the next callback, pass it directly as the subscribe argument.

// Pass the next callback function directly
const sub = source$.subscribe(
   apple => console.log(`Apple was emitted ${apple}`)
);
Enter fullscreen mode Exit fullscreen mode

Pass an Observer Object

Since you can only pass one object to subscribe, if you need to handle multiple types of notifications, pass an Observer object with the desired set of callbacks.

// Pass an Observer object with callback arrow functions
const sub = source$.subscribe({
  next: apple => console.log(`Apple was emitted ${apple}`),
  error: err => console.log(`Error occurred: ${err}`),
  complete: () => console.log(`No more apples, go home`)
});
Enter fullscreen mode Exit fullscreen mode

Notice that the above code passes an object into the subscribe method with next, error, and complete methods. You only need to specify the methods for the notifications you'll handle. So if you don't need to process the complete notification, you don't need to specify it.

What if You Don't Want to Use Arrow Functions?

The prior examples all used arrow functions, denoted with =>. Some developers may prefer to use declared named functions instead of arrow functions when defining Observer callbacks. Like this:

const sub = source$.subscribe({
  next(apple) { console.log(`Apple was emitted ${apple}`) },
  error(err) { console.log(`Error occurred: ${err}`)},
  complete() { console.log(`No more apples, go home`)}
});
Enter fullscreen mode Exit fullscreen mode

Notice the syntax difference. Here we define each function (next) with it's parameter (apple) and function body denoted with {}.

But watch out for this. In TypeScript (and in JavaScript), this is scoped to the function. So if you have code like the following:

// Watch out for `this`
const sub = source$.subscribe({
  next(apple) { this.apple = apple }, // Does NOT reference the 
                                      // class-level variable
  error(err) { console.log(`Error occurred: ${err}`)},
  complete() { console.log(`No more apples, go home`)}
});
Enter fullscreen mode Exit fullscreen mode

It may not work as expected. The this.apple will not reference a class-level variable and will instead define a function-scoped variable.

How Do the Pieces Fit Together?

The Observable, Observer, and Subscription work together to:

  • Tell the Observable to start emissions/notifications
  • Provide callback functions to react to those emissions/notifications
  • Set up a subscription that allows for unsubscribing

Alt Text

Here are the concepts shown on a more formal marble diagram.
Alt Text

Thanks to @michael_hladky for this marble diagram.

Here is a more common example usage in an Angular application.

Service

  products$ = this.http.get<Product[]>(this.productsUrl)
    .pipe(
      tap(data => console.log(JSON.stringify(data))),
      catchError(this.handleError)
    );
Enter fullscreen mode Exit fullscreen mode

In the above code, products$ represents the Observable.

It is a common practice to add a $ suffix to a variable that represents an Observable. This makes it easier to see when the code is working with an Observable.

Component

ngOnInit(): void {
  this.sub = this.productService.products$.subscribe({
      next: products => this.products = products,
      error: err => this.errorMessage = err
    });
}
Enter fullscreen mode Exit fullscreen mode

In the component, the Observer object is passed into the subscribe method, defining two callbacks: next and error.

The this.sub represents the Subscription returned from the subscribe method. This is used to unsubscribe on ngOnDestroy.

I hope that clarified the meaning of the term Observer and demonstrates how three key RxJS concepts: Observable, Subscription, and Observer work together.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .