Sekrab Garage

Angular state management simple solution

RxJS based state management in Angular - Part I

AngularDesign February 15, 22
Subscribe to Sekrab Parts newsletter.
Series:

Simple State Management

We are creating a class, that contains the minimum required, to handle Angular state management using RxJS, and testing few other methods as we go along.
  1. two years ago
    RxJS based state management in Angular - Part I
  2. two years ago
    RxJS based state management in Angular - Part II
  3. two years ago
    RxJS based state management in Angular - Part III
  4. two years ago
    RxJS based state management in Angular - Part IV
  5. two years ago
    RxJS based state management in Angular - Part V

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, I am here to explore and implement the RxJS based solution.

anchorThe problem

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

anchorThe 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.

anchorStart here, and work backwards

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

The component

<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.

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:

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.

// 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:

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:

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.

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

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.

appendList(items: T[]) {
    // update current list
    const currentList = this.currentList.concat(items);
    this.stateList.next(currentList);
}

We might also need to prepend an item:

prependItem(item: T): void {
  this.stateList.next([item, ...this.currentList]);
}

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

anchorExample: 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.

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

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:

@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.

  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$

<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>

anchorUpdating 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.

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

 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

 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.

  1. two years ago
    RxJS based state management in Angular - Part I
  2. two years ago
    RxJS based state management in Angular - Part II
  3. two years ago
    RxJS based state management in Angular - Part III
  4. two years ago
    RxJS based state management in Angular - Part IV
  5. two years ago
    RxJS based state management in Angular - Part V