From Imperative to Declarative Angular Development with RxJS
Tags: angular, rxjs, declarative, imperative
Published: 2024-05-16 11:23:54

Photo by <a href="https://unsplash.com/@ourselp?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Philippe Oursel</a> on <a href="https://unsplash.com/photos/man-wearing-black-uniform-r3GdpsTgxM4?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
  
This article exemplifies declarative data access in Angular using the RxJS flattening operator.

## Background Story
_Picture yourself as the captain of a ship. Your crew frequently seeks guidance. As new tasks emerge, repeating instructions leads to chaos._

- _As an **Imperative** Captain, you may notice these shortcomings but still choose to continue **repeating how** to do something._

- _As a **Declarative** Captain, you recognize the need for change, as your daily life on board will become exhausting. You take the initiative to gather the crew and decisively **define what** needs to be done once and for all._

## Back to Angular
The Angular component is responsible for displaying a list of auctions based on the query state.

### Query Type
`AuctionQuery`
```javascript
export interface AuctionQuery extends PaginationQuery {
    itemTitle: string | null,
}
```

```javascript
export interface PaginationQuery {
    page: number,
    itemsPerPage: number
}
```

### Response Type
`PaginationResponse<AuctionModel>`
```javascript
export interface AuctionModel {
    id: number
    itemTitle: string
    start: Date
    startingBid: number
}
```

```javascript
export type PaginationResponse<T> = {
    count: number,
    rows: T[]
}
```

### Component Template

The template includes:

- Search component

- List of auctions matching the query

- Pagination component

## Imperative
This is how Imperative Captain does things.

Take note of the auctions property, as it serves as the component's data.

TS:
```javascript
export class AuctionListComponent implements OnInit, OnDestroy {

  auctions: PaginationResponse<AuctionModel> | null = null;
  itemsPerPage = this.auctionService.query.itemsPerPage;
  subscriptions = new Subscription();

  constructor(private auctionService: AuctionService) { }

  ngOnInit(): void {
    this.setAuctions();
  }
  
  setAuctions() {
    this.subscriptions.add(
      this.auctionService.getAuctions().pipe(
        tap({
          next: (v: PaginationResponse<AuctionModel>) => {
            this.auctions = v;
          }
        })
      ).subscribe()
    );
  }

  queryPage(page: number) {
    this.auctionService.query.page = page;
    this.setAuctions();
  }

  querySearch(term: string) {
    this.auctionService.query.page = 1;
    this.auctionService.query.itemTitle = term;
    this.setAuctions();
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }
```

Service:
```javascript
export class AuctionService {

  query: AuctionQuery = {
    itemTitle: null,
    page: 1,
    itemsPerPage: 3
  }

  ...

  getAuctions(): Observable<PaginationResponse<AuctionModel>> {
    const options = { params: this.createHttpParams(this.query) };
    return this.http.get<PaginationResponse<AuctionModel>>(this.apiUrl, options).pipe(
      catchError((err: HttpErrorResponse) => {
        throw new Error('could not load data');
      })
    )
  }
}
```

Template:
```javascript
<app-search (termChange)="querySearch($event)"></app-search>
<div class="auctions-container">
    @if (auctions) {
        @for (auction of auctions.rows; track $index) {
            <app-auction [auction]="auction"></app-auction>
        }
    }
</div>
@if (auctions) {
    <app-pagination 
        [recordsPerPage]="itemsPerPage"
        [totalRecords]="auctions.count" 
        (pageSelected)="queryPage($event)">
    </app-pagination>
}
```
### Flaws
- Reassigning property at different places in the code leads to poor readability. It's not clear how property changes over time.
- Manual handling of subscriptions.

## Declarative
This is how Declarative Captain does things.

We need to clearly define the data source during component initialization. This definition should also include the query state, marking the shift from an imperative (how) to a declarative (what) approach.

First, we need observable emitting query updates:
```javascript
export class AuctionQueryObservable {

    query: AuctionQuery = {
        itemTitle: null,
        page: 1,
        itemsPerPage: 3
    }
    querySubject = new BehaviorSubject<AuctionQuery>(this.query);

    titleUpdate(title: string){
        this.query.page = 1;
        this.query.itemTitle = title;
        this.querySubject.next(this.query);
    }

    pageUpdate(page: number){
        this.query.page = page;
        this.querySubject.next(this.query);
    }
}
```
Now let's see how we can use observable query:
```javascript
export class AuctionService {

  queryWithSubject = new AuctionQueryObservable();
  ...

  getAuctions(): Observable<PaginationResponse<AuctionModel>> {
    return this.queryWithSubject.querySubject.pipe(
      switchMap(updatedQuery => {
        const options = { params: this.createHttpParams(updatedQuery) };
        return this.http.get<PaginationResponse<AuctionModel>>(this.apiUrl, options).pipe(
          catchError((err: HttpErrorResponse) => {
            throw new Error('could not load data');
          })
        )
      })
    ) 
  }
}
```
We are utilizing the **switchMap** (RxJS flattening operator):
On each query change, the previous inner observable (the result of the passed function) is canceled, and the new observable is subscribed.

TS:
```javascript
export class AuctionListComponent {

  auctions$ = this.auctionService.getAuctions();
  itemsPerPage = this.auctionService.queryWithSubject.query.itemsPerPage;

  constructor(private auctionService: AuctionService) { }

  querySearch(term: string){
    this.auctionService.queryWithSubject.titleUpdate(term);
  }

  queryPage(page: number){
    this.auctionService.queryWithSubject.pageUpdate(page);
  }

}
```
The way we defined auctions allows us to utilize async pipe for handling subscriptions.

Template:
```javascript
<app-search (termChange)="querySearch($event)"></app-search>
@if(auctions$ | async; as auctions){
    @if(auctions.count > 0){
        <div class="auctions-container">
            @for(auction of auctions.rows; track auction.id) {
                <app-auction [auction]="auction"></app-auction>
            }
        </div>
        <app-pagination 
            [recordsPerPage]="itemsPerPage"
            [totalRecords]="auctions.count" 
            (pageSelected)="queryPage($event)">
        </app-pagination>
    } @else {
        <h2>nothing found</h2>
    }
}
```

## Additionally
We can use **signals** (from version 16 or 17) to monitor the query changes. The aim is to encapsulate a value and notify the consumer of any alterations. This solution, while similar to the previous, is more conventional and unlocks new possibilities.

Instead of Behaviour Subject:

```javascript
export class AuctionQueryService {
    query = signal<AuctionQuery>({
        itemTitle: null,
        page: 1,
        itemsPerPage: 3
    })

    titleUpdate(itemTitle: string){
        this.query.update((query)=>{
            return {
                ...query,
                page:1,
                itemTitle,
            }
        })
    }

    pageUpdate(page: number){
        this.query.update((query)=>{
            return {
                ...query,
                page
            }
        })
    }
}
```

```javascript
export class AuctionService {
  ...

  getAuctions(query: AuctionQuery): Observable<PaginationResponse<AuctionModel>> {
        const options = { params: this.createHttpParams(query) };
        return this.http.get<PaginationResponse<AuctionModel>>(this.apiUrl, options).pipe(
          catchError((err: HttpErrorResponse) => {
            throw new Error('could not load data');
          })
        )
  }

}
```
Updated List Component:

```javascript
export class AuctionListComponent {

  auctions = toSignal(
    toObservable(this.auctionQueryService.query).pipe(
      switchMap((query: AuctionQuery) => {
        return this.auctionService.getAuctions(query);
      })
  ));
  itemsPerPage = computed(()=>{
    return this.auctionQueryService.query().itemsPerPage;
  })
  totalRecords = computed(()=>{
    return this.auctions()?.count || 0;
  })

  constructor(private auctionService: AuctionService, private auctionQueryService: AuctionQueryService) { }

  querySearch(term: string){
    this.auctionQueryService.titleUpdate(term);
  }

  queryPage(page: number){
    this.auctionQueryService.pageUpdate(page);
  }

}
```

The signal is converted to an observable using the **toObservable** function so we can continue the pipe and use the **switchMap** operator. 

Signals have the advantage of being created from existing ones using the **computed** function. That's why _auctions_ property is eventually converted to a signal using the **toSignal** function (and also for consistency).

Finally, changes are needed on the template because we are using signals instead of observables:

```javascript
<app-search (termChange)="querySearch($event)"></app-search>
<div class="auctions-container">
    @if(totalRecords() > 0){
        <div class="auctions-container">
            @for(auction of auctions()?.rows; track auction.id) {
                <app-auction [auction]="auction"></app-auction>
            }
        </div>
        <app-pagination 
            [recordsPerPage]="itemsPerPage()"
            [totalRecords]="totalRecords()" 
            (pageSelected)="queryPage($event)">
        </app-pagination>
    } @else {
        <h2>nothing found</h2>
    }
</div>
```

## Conclusion
It's not necessary to be entirely declarative at all times. Declarative code is essentially imperative under the hood, but it's often more efficient to specify what we want and let the framework handle it. By telling _what_, the code can be cleaner and more extendable.

_I hope this will be helpful to someone. I'm here to learn as well, so any suggestions are welcome._