6

The classic pattern for data access in Angular suggests code like this:

Classic Data Access Pattern

getProducts(): Observable<Product[]> {
  return this.http.get<Product[]>(this.productsUrl)
    .pipe(
      tap(data => console.log(JSON.stringify(data))),
      catchError(this.handleError)
    );

Above we have a method the returns an Observable. When the data is returned from the http endpoint, that Observable emits the data in a shape defined by the Product[] generic parameter where Product is an interface.

For a more reactive application, the declarative/reactive approach is often suggested. It changes the above method to a property declaration like this:

Reactive Data Access Pattern

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

Notice that this declares a property in the service for the Observable instead of a method. The $ suffix on products$ is a convention used to distinguish properties that are Observables vs other types of data structures such as general objects or arrays.

This pattern is often used when there is UI interactivity (such as filtering based on a selection), when working with complex data (such as combining data from multiple end points) and when sharing data across components (such as when each component needs to react when the data changes).

The reactive data access pattern presents a common problem. How do we "pass" data required by the URL when using this technique?

For example, say that the http request requires a category to only pull down products for that category:

this.http.get<Product[]>(`${this.url}?cat=${catId}`)

How can this category be "passed in" since there is no method?

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • 1
    NOTE: Followed the directive here for this question and answer: https://stackoverflow.com/help/self-answer – DeborahK Jul 25 '21 at 17:44

1 Answers1

14

Instead of thinking how to "pass" data, think instead about how to "emit" that value over time. To emit data, we define our own Observable using Subject or BehaviorSubject.

private categorySubject = new Subject<number>();
categorySelectedAction$ = this.categorySubject.asObservable();

products$ = this.categorySelectedAction$.pipe(
 switchMap(catId=>this.http.get<Product[]>(`${this.url}?cat=${catId}`))
    .pipe(
      tap(data => console.log(data)),
      catchError(this.handleError)
  ));

selectedCategoryChanged(categoryId: number): void {
  this.categorySubject.next(categoryId);
}

In the above code, we define our Observable using a Subject. We declare the Subject private so it cannot be accessed from outside our service. We then expose the read-only part of that Subject using asObservable().

Whenever the user selects a different category, the component calls selectedCategoryChanged which emits the selected categoryId into the stream defined by our Subject.

For products$, when a value is emitted, it is piped through a set of operators. The switchMap is a higher-order mapping operator that automatically subscribes to the inner Observable (the this.http.get...) and flattens the result. The returned products for the specified category are then emitted into the products$ stream.

To see a more complete solution, see this github: https://github.com/DeborahK/Angular-RxJS/tree/master/APM-Final

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • 1
    Thanks for the tip, Deborah! This is my go to approach these days! – katesky8 Jul 24 '21 at 01:02
  • 1
    Sorry, but It is a weird answer to the weird question. You don't need Subject or BehaviorSubject if you're going to use your data in one component only. If you want to know how to pass params to your request, you have to check documentation for your 'this.http' client. – Dmitry Grinko Jul 24 '21 at 01:37
  • 1
    Hi @dmitry-grinko - For simple cases you are right. The post says "For a more reactive application", implying an app that requires more component interactions. I'll add more detail about this when I'm back at my desk. Thanks! – DeborahK Jul 24 '21 at 05:51
  • Quick question: is the `categorySelectedAction$` property necessary? You can assign `products$ = this.categorySubject.pipe(...` which infers the no longer needed `.asObservable()`. It also keeps `categorySubject` private while `products$` remains public. – Joshua McCarthy Jul 24 '21 at 06:15
  • Stand alone for this snippet you are correct. But the assumption is that for a more interactive application, other components will also want to subscribe to the `categorySelectedAction$` to change header component text "Products for Category x" or change "subcategory" options, etc. – DeborahK Jul 24 '21 at 15:16
  • 1
    Right, this was more of a sanity check on my behalf. By the way, thank you for your content. It definitely gave me a boost in understanding declarative RxJS back when I was first learning it. – Joshua McCarthy Jul 24 '21 at 23:22
  • What about using NgRX store? where instead of emitting category id by subject next just dispatch the action. I would definitely go with dispatching an action new bees can read about ngrx store https://ngrx.io/guide/store – Kamran Khatti Jul 25 '21 at 08:25
  • 1
    Yes @KamranKhatti the NgRx store would be a valid option. But that requires buying into and learning the NgRx library. Plus it's hard to use it for little code snippets. :-) I have an NgRx example here: https://github.com/DeborahK/Angular-NgRx-GettingStarted/tree/master/APM-Demo4 – DeborahK Jul 25 '21 at 17:33
  • What if you have more than one filter? for example price.greaterThan. Is it good practice to use a filter object as Subject value instead? – Cristian18 Oct 05 '21 at 00:48
  • 1
    Yes, you could define an interface and specify the multiple filter fields as one object. Or if other parts of the application need to react to the different fields, you could define multiple subjects. Here is an example that does paging and filtering: https://github.com/DeborahK/Angular-ActionStreams – DeborahK Oct 05 '21 at 22:29
  • Assuming that the code in your solution is in the component, do you have to make the http get call from there? What if you want to make that call from a service? Is that possible with the reactive pattern? – gsmith140 Jul 20 '22 at 20:22