RxJS based state management in Angular - Part I
Tags: angular, design, programming
Published: 2022-02-15 16:45:37

Google it, Angular State Management, odds are, you will end up on an ngRx solution. Referring to this greate article [Choosing the State Management Approach in Angular App](https://blog.clairvoyantsoft.com/choosing-the-state-management-approach-in-angular-app-7080aac20378), I am here to explore and implement the RxJS based solution.
> [On stackblitz](https://angular-state-local.stackblitz.io)

> [On Sekrab Garage](https://garage.sekrab.com/posts/rxjs-based-angular-state-management)

### The problem:
If you are here, you know the problem state management fixes, most probably!

### The solution:
One of the approaches to design a solution, is working your way backwards. Given a template, that represents visual components, what do we need to get state organized? 

Here is a quick example, say we have a list of records, with basic delete, add and edit functionalities. Most often than not, the functionalities occur in sub routes, or child components. In this part, I want to explore the very basic RxJS state functionality. In future parts (I am hoping), will be adding extra functionalities, and some twist in scenarios. The idea is, stay simple, we do not want to run to NgRX, just yet.

### Start here, and work backwards

```typescript
this.records$ = this.recordService.GetList().pipe(
    switchMap(rcs => this.recordState.doSomethingToInitializeState(rcs))
);
``` 
The component

```html
<ng-container *ngIf="records$ | async as records">
  <ul>
    <li *ngFor="let record of records">
      <a (click)="editRecord(record)">{{ record.prop }}</a>
      <a (click)="delete(record)">Delete</a>
    <li>
  </ul>
</ng-container>
```
For simplicity, let us assume that the components that handle creating and editing (the form components) are loaded on the same route, for example, in a dialog. Thus the main list of records does not get reloaded, nor the OnInit fired again.

```typescript
this.recordService.SaveRecord({...record}).subscribe({
 next: (success) => this.recordState.editOneItemState(record)
});

this.recordService.CreateRecord({...newRecord}).subscribe({
next: (successRecord) => this.recordState.addNewItemToState(successRecord)
});

this.recordService.DeleteRecord({...record}).subscribe({
next: (success) => this.recordState.deleteItemFromState(record);
});
```

The record service should take care of getting from server or API. So first step is to load the list into state, then to allow editing, deletion and appending of new items. Our state should look like this:

```typescript
class State {
   doSomethingToInitializeState(){ ... }

   editOneItemState(item) {...}

   addNewItemToState(item) {...}

   deleteItemFromState(item) {...}
}
```

What RxJs provides, is a `BehaviorSubject` exposed `asObservable`, this subject, is what gets updated (via `next` method). Let's name our objects properly from now on. The subject shall be named `stateList`, because it represents the list of the elements to be added to the state.

```typescript
// internal BehaviorSubject initiated with an empty array (safest solution)
private stateList: BehaviorSubject<Record[]> = new BehaviorSubject([]);

// exposed as an observable
stateList$: Observable<Record[]> = this.stateList.asObservable(); // optionally pipe to shareReplay(1)
```
Let's initiate, add, update, and delete, properly:

```typescript
SetList(items: Record[]): Observable<Record[]> {
   // first time, next items as is
   this.stateList.next(items);
   // return ready to use observable 
   return this.stateList$;
}
```
One of the cool features of `BehaviorSubject` is the `getValue()` of the current subject, so let me define a getter for the current list:

```typescript
get currentList(): Record[] {
    return this.stateList.getValue();
}
```
But before we carry on, let us build this class upon a generic, so we can make as many states as we wish later. 

```typescript
export class StateService<T>  {
    // private now is protected to give access to inheriting state services
    protected stateList: BehaviorSubject<T[]> = new BehaviorSubject([]);
    stateList$: Observable<T[]> = this.stateList.asObservable().pipe(shareReplay(1));

    SetList(items: T[]): Observable<T[]> {
        this.stateList.next(items);
        return this.stateList$;
    }
    
    get currentList(): T[] {
        return this.stateList.getValue();
     }

    // add item, by cloning the current list with the new item
    addItem(item: T): void {
        this.stateList.next([...this.currentList, item]);
    }

    // edit item, by finding the item by id, clone the list with the 
    // updated item (see note below)
    editItem(item: T): void {
        const currentList = this.currentList;
        const index = currentList.findIndex(n => n.id === item.id);
        if (index > -1) {
            currentList[index] = clone(item); // use a proper cloner
            this.stateList.next([...currentList]);
        }
    }

    // find item by id then clone the list without it
    removeItem(item: T): void {
        this.stateList.next(this.currentList.filter(n => n.id !== item.id));
    }
}
```
To make sure ID exists, we can extend T to a generic interface like this

```typescript
export interface IState {
    id: string; 
}

export class StateService<T extends IState>  { ... }
```

As you figured, think state? think immutable. Always clone. In the above, you can use **lodash clone** function (install the clone function alone), or you can do as I always do, just copy over the code into your source code 😂! Happy, in control life. The stackblitz project has that clone ready in core/common.ts

These basic members are good enough for our basic uses, one more thing to cover is allowing the list to grow by appending new items to it (think continuous pagination), thus the need to append new elements to state list.

```typescript
appendList(items: T[]) {
        // update current list
        const currentList = this.currentList.concat(items);
        this.stateList.next(currentList);
}
```
We might also need to prepend an item:
```typescript
prependItem(item: T): void {
        this.stateList.next([item, ...this.currentList]);
 }
```

There are other functionalities to include but we will stop here to implement.

### Example: list of transactions, add, edit, and delete

#### Transaction Service
First, the transaction service with the CRUD, assuming the `HttpService` is either the `HttpClient` or any other provider of your choice, for example Firestore. The stackblitz project works with a local json array in `mock-data` folder. 

> The HttpService is where caching data and handling cache is supposed to go, this part is left out as it is not the scope of state management using RxJS.

```typescript
import { ITransaction, Transaction } from '../services/transaction.model';
import { HttpService } from '../core/http';

@Injectable({ providedIn: 'root' })
export class TransactionService {
  private _listUrl = '/transactions';
  private _detailsUrl = '/transactions/:id';
  private _createUrl = '/transactions';
  private _saveUrl = '/transactions/:id';
  private _deleteUrl = '/transactions/:id';

  constructor(private _http: HttpService) {}

  GetTransactions(options: any = {}): Observable<ITransaction[]> {
    // we'll make use of options later
    const _url = this._listUrl;

    return this._http.get(_url).pipe(
      map((response) => {
        return Transaction.NewInstances(<any>response);
      })
    );
  }

  GetTransaction(id: string): Observable<ITransaction> {
    const _url = this._detailsUrl.replace(':id', id);
    return this._http.get(_url).pipe(
      map((response) => {
        return Transaction.NewInstance(response);
      })
    );
  }

  CreateTransaction(transaction: ITransaction): Observable<ITransaction> {
    const _url = this._createUrl;
    const data = Transaction.PrepCreate(transaction);

    return this._http.post(_url, data).pipe(
      map((response) => {
        return Transaction.NewInstance(<any>response);
      })
    );
  }

  SaveTransaction(transaction: ITransaction): Observable<ITransaction> {
    const _url = this._saveUrl.replace(':id', transaction.id);
    const data = Transaction.PrepSave(transaction);

    return this._http.put(_url, data).pipe(
      map((response) => {
        return transaction;
      })
    );
  }

  DeleteTransaction(transaction: ITransaction): Observable<boolean> {
    const _url = this._deleteUrl.replace(':id', transaction.id);

    return this._http.delete(_url).pipe(
      map((response) => {
        return true;
      })
    );
  }
}
```
#### Transaction Model, the basics
```typescript
import { makeDate } from '../core/common';

export interface ITransaction {
  id: string; // important to extend IState interface
  date: Date;
  amount: number;
  category: string;
  label: string;
}

export class Transaction implements ITransaction {
  id: string;
  date: Date;
  amount: number;
  category: string;
  label: string;

  public static NewInstance(transaction: any): ITransaction {
    return {
      id: transaction.id,
      date: makeDate(transaction.date),
      amount: transaction.amount,
      category: transaction.category,
      label: transaction.label,
    };
  }

  public static NewInstances(transactions: any[]): ITransaction[] {
    return transactions.map(Transaction.NewInstance);
  }

  // prepare to POST
  public static PrepCreate(transaction: ITransaction): any {
    return {
      date: transaction.date,
      label: transaction.label,
      category: transaction.category,
      amount: transaction.amount,
    };
  }
  // prepare to PUT
  public static PrepSave(transaction: ITransaction): any {
    return {
      date: transaction.date,
      label: transaction.label,
      category: transaction.category,
      amount: transaction.amount,
    };
  }
}
```
#### The Transaction State Service:
```typescript
@Injectable({ providedIn: 'root' })
export class TransactionState extends StateService<ITransaction> {
  // one day, I will have a rich method that does something to state
 }
}
```
Now inside the list component, all we have to do, is get transactions, and load state.
```typescript
tx$: Observable<ITransaction[]>;
constructor(
    private txState: TransactionState,
    private txService: TransactionService
  ) {}

  ngOnInit(): void {
    this.tx$ = this.txService
      .GetTransactions()
      .pipe(switchMap((txs) => this.txState.SetList(txs)));
  }
```
In the template, subscribe to your tx$
```html
<ul  *ngIf="tx$ | async as txs">
  <li *ngFor="let tx of txs;">
    <div class="card">
        <div class="small light">{{tx.date | date}}</div>
        {{tx.label }}
        <div class="smaller lighter">{{ tx.category }}</div>
       <strong>{{ tx.amount }}</strong>
    </div>
  </li>
</ul>
```
### Updating state
To add an element, I am not going in details of the form that creates the new transaction, so we will create a random transaction upon clicking the button, but to make a point, in stackblitz project I will place these buttons in a child component.
```typescript
append(): void {
    // this functionality can be carried out anywhere in the app
    this.txService.CreateTransaction(newSample()).subscribe({
      next: (newTx) => {
        // update state
        this.txState.addItem(newTx);
      },
      error: (er) => {
        console.log(er);
      },
    });
  }
  prepend(): void {
    // prepend to list
    this.txService.CreateTransaction(newSample()).subscribe({
      next: (newTx) => {
        // update state
        this.txState.prependItem(newTx);
      },
      error: (er) => {
        console.log(er);
      },
    });
  }
```
Delete, cute and simple
```typescript
 delete(tx: ITransaction): void {
    // this also can be done from a child component
    this.txService.DeleteTransaction(tx).subscribe({
      next: () => {
        this.txState.removeItem(tx);
      },
      error: (er) => {
        console.log(er);
      },
    });
  }
```
Edit
```typescript
 edit() {
    // steer away from bad habits, always clone
    const newTx = { ...this.tx, date: new Date() };
    this.txService.SaveTransaction(newTx).subscribe({
      next: () => {
        this.txState.editItem(newTx);
      },
      error: (er) => {
        console.log(er);
      },
    });
  }
```
This was an example of a root service that gets loaded on a root component, but sometimes, there can be multiple individual instances, or a state of a single object. Coming up, I hope, I will dive a bit deeper with the pagination example.

#### What do you think? your comments and feedback is most welcome.

Resouces:
- [On stackblitz](https://angular-state-local.stackblitz.io)
- [Choosing the State Management Approach in Angular App](https://blog.clairvoyantsoft.com/choosing-the-state-management-approach-in-angular-app-7080aac20378)
- [On Sekrab Garage](https://garage.sekrab.com/posts/rxjs-based-angular-state-management)