DEV Community

Cover image for RxJS based state management in Angular - Part III
Ayyash
Ayyash

Posted on • Updated on

RxJS based state management in Angular - Part III

I am writing this part, knowing there will be an IV, because I am drifting away, experimenting more features. Last time we spoke, I told you we have a challenge of keeping up with the total number of records coming from the server, and updating it when user adds or removes. So let's work backwards and see how the end result should look like.

Challenge: a state of a list and a single object

Even though we agreed not to do that, to keep it simple, but I am experimenting just to confirm that it is indeed unnecessary complication. Let's add a Total in our template, and rewrap the content a bit

<!-- wrap it inside a container -->
<ng-container *ngIf="tx$ | async as txs">
    <!-- add placeholder for total -->
    <div>
        Total: {{dbTotalHere}}
    </div>
<ul class="rowlist spaced">
    <li *ngFor="let tx of txs;">
        <div class="card">
            <span class="rbreath a" (click)="delete(tx)">🚮</span>
            <div class="content">
                <div class="small light">{{tx.date | date}}</div>
                {{tx.label }}
                <div class="smaller lighter">{{ tx.category }}</div>
            </div>
            <div class="tail"><strong>{{ tx.amount }}</strong></div>
        </div>
    </li>
</ul>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

In component, I expect the matches and the total to come back together in a list, so the model eventually looks like this

// returned data from db usually has a total, or count, in addition to the items matched to query
export interface IList<T> {
    total: number;
    matches: T[];
}
Enter fullscreen mode Exit fullscreen mode

And here is the update on the transaction service and model

GetTransactions(options: any = {}): Observable<IList<ITransaction>> {
    // turn options into query string and add them to url (out of scope)
    const _url = this._listUrl + GetParamsAsString(options);

    return this._http.get(_url).pipe(
      map((response) => {
        // map the result to proper IList
        return Transaction.NewList(<any>response);
      })
    );
  }
Enter fullscreen mode Exit fullscreen mode

In the Transaction model, we just need to create the NewList mapper:

public static NewList(dataset: any): IList<ITransaction> {
    return {
        total: dataset.total,
        matches: Transaction.NewInstances(dataset.matches)
    };
}        
Enter fullscreen mode Exit fullscreen mode

So what if, we create a state of the IList<T>?

Complication: The extra generic (in addition to the StateService generic)

Complication: now IList must extend IState, which must have an id prop. Totally rubbish! But let's go on.

The ListState service

@Injectable({providedIn: 'root'})
export class ListState<T> extends StateService<IList<T>> {
   // to add new extended features here
}
Enter fullscreen mode Exit fullscreen mode

Now back to our component, and see what we need

// new tx state (n for new, because no one is looking)
nTx$: Observable<IList<ITransaction>>;
constructor(
        private txService: TransactionService,
        private paramState: ParamState,
        // our new state
        private listState: ListState<ITranscation>
    ) { }

    ngOnInit(): void {
        // watch param changes to return the matches
        this.nTx$ = this.paramState.stateItem$.pipe(
            switchMap((state) => this.txService.GetTransactions(state)),
            switchMap(txs => {
                // here I need to "append" to the internal matches, and update "total"
                return this.listState.updateListState(txs);
            })
        );
        // but, I need to set the total and matches to an empty array first
        this.listState.SetState({
            total: 0,
            matches: []
        });

        // setoff state for first time
        this.paramState.SetState({
            page: 1,
            size: 5,
        });
}
Enter fullscreen mode Exit fullscreen mode

And the component

<ng-container *ngIf="nTx$ | async as nTx">
    <!-- get total -->
    <div class="spaced bthin">
        Total {{ nTx.total }}
    </div>
    <!-- get matches -->
    <ul class="rowlist spaced">
        <li *ngFor="let tx of nTx.matches">
            ... as is
        </li>
    </ul>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

When user adds:

    add(): void {
        this.txService.CreateTx(newSample()).subscribe({
            next: (newTx) => {
                // add to internal matches and update total
                this.listState.addMatch(newTx);
            },
            error: (er) => {
                console.log(er);
            },
        });
    }
Enter fullscreen mode Exit fullscreen mode

Let's stop here and see what we need. We need to extend the functionality of the List State so that the internal matches array, is the one that gets updated with new additions, and the total count, is updated with a +1 or -1.

image.png

Yesterday I went to sleep on that, I dreamed that I should be able to have the same model dealing with the array, and the total, if it does not make sense, it is not supposed to, it was a dream!

Complication If the total is being updated by other means, like server polling where multiple users are affecting the total, our state has to keep track, but honestly if we reach a point where it matters, we should go a different path, or run to NgRx (although I don't think they have the solution out of the box, but you will feel less guilty in front of your team mates!)

Complication Now we have to cast T to "any" or IState before we use "id" on it. More rubbish! Bet let's keep going.

The List state service:

@Injectable({providedIn: 'root'})
export class ListState<T> extends StateService<IList<T>> {

    updateListState(item: IList<T>): Observable<IList<T>> {
        // append to internal matches and update total, the return state
        const newMatches = [...this.currentItem.matches, ...item.matches];
        this.stateItem.next({matches: newMatches, total: item.total});
        return this.stateItem$;
    }

    addMatch(item: T) {

        // add item to matches, next state, also adjust total
        const newMatches = [...this.currentItem.matches, item];
        this.stateItem.next({matches: newMatches, total: this.currentItem.total + 1});
    }

    removeMatch(item: T) {
        // remove item from matches, next state, also adjust total
        // casting to "any" is not cool
        const newMatches = this.currentItem.matches.filter(n => (<any>n).id !== (<any>item).id);
        this.stateItem.next({matches: newMatches, total: this.currentItem.total - 1});
    }

    editMatch(item: T) {
        // edit item in matches, next state
        const currentMatches = [...this.currentItem.matches];
        const index = currentMatches.findIndex(n => (<any>n).id === (<any>item).id);
        if (index > -1) {
            currentMatches[index] = clone(item);
            this.stateItem.next({...this.currentItem, matches: currentMatches});
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

As you can see, we have driven our simple state a bit deeper, and used practically the same methods on a deeper level. Not cool. But, on the other hand, I like the idea of having the original abstract state itself, a state of IList where the matches is a sub property. This can be more useful even if we want to create a state of a simple array, all we have to do is place the array in a pseudo model with matches property.

*That note aside, let's back up a bit, and try something different. What if we use param state to hold the total? *

Challenge: tangling states

First, we have to retrieve the total from the returned server call. In the list component:

      // we are back to tx, not nTx, if you were paying attention
       this.tx$ = this.paramState.stateItem$.pipe(
            switchMap((state) => this.txService.GetTransactions(state)),
            switchMap((txs) => {
                // HERE: before we append the list of matches, let's update paramState with total
                // but... you cannot update state in the pipe that listens to the same state!
                this.paramState.UpdateState({total: txs.total});
                return this.txState.appendList(txs.matches)}),
        );

       // now that we are appending to list, need to first empty list
       this.txState.SetList([]);

       // setoff state for first time
        this.paramState.SetState({
            page: 1,
            size: 5,
            total: 0 // new member
        });
Enter fullscreen mode Exit fullscreen mode

And when we add or remove an item, again, we need to update param state:

    add(): void {

        this.txService.CreateTx(newSample()).subscribe({
            next: (newTx) => {
                // update state, watch who's listening
                this.paramState.UpdateState({total: this.paramState.currentItem.total+1});
                this.txState.addItem(newTx);
            },
            error: (er) => {
                console.log(er);
            },
        });
    }
     delete(tx: ITx): void {

        this.txService.DeleteTx(tx).subscribe({
            next: () => {
                // update state
                this.paramState.UpdateState({total: this.paramState.currentItem.total-1});
                this.txState.removeItem(tx);
            },
            error: (er) => {
                console.log(er);
            },
        });
    }
Enter fullscreen mode Exit fullscreen mode

Every time we update param state, we fire a GetTransactions call. One temptation to fix that is to update the currentItem variables directly. But that would be wrong. The currentItem in our state has a getter and no setter, for a purpose. We do not want to statically update internal value, we always want to update state by next-ing the subject. Though Javascript, and cousin Typescript, would not object on setting a property of an object. The other better option is to rely on RxJS's distinctUntilKeyChanged

      this.tx$ = this.paramState.stateItem$.pipe(
            // only when page changes, get new records
            distinctUntilKeyChanged('page'),
            switchMap((state) => this.txService.GetTxs(state)),
            switchMap((txs) => {
                // if you are worried coming back from server, the total is not up to date
                // update state only if page = 1
                this.paramState.UpdateState({total: txs.total});
                return this.txState.appendList(txs.matches)}),
        );

Enter fullscreen mode Exit fullscreen mode

Another solution, now that we have a state class, is create a separate state for total. You might think it's aweful, but another property may also need to be kept track of, "has more to load" property.

Note, the ngOnOnit is called only when page loads, and that is where the initial list need to be emptied, otherwise revisiting the same page will append to a previous list.

Let's look into a scenario of having multiple states of the same service. But first..

Fix the id rubbish

Let's get rid of the extra id inIState by splitting the state class to two distinctive classes: StateService and ListStateService. Note: I ditched the state service created above as an experiment.

// the ListStateService with generic extending IState
export class ListStateService<T extends IState>  {

    protected stateList: BehaviorSubject<T[]> = new BehaviorSubject([]);
    stateList$: Observable<T[]> = this.stateList.asObservable();

    // ...
}

// the StateService fixed to have a generic with no complications
export class StateService<T>  {

    protected stateItem: BehaviorSubject<T | null> = new BehaviorSubject(null);
    stateItem$: Observable<T | null> = this.stateItem.asObservable();
   // ...
}
Enter fullscreen mode Exit fullscreen mode

Next Tuesday

I hope you're still following. Next week I will be investigating the local state and the "has more" feature for pagination. If you have any questions or comments, let me know in the comments section (wherever it might be, depending on where you're seeing this 🙂)

Code demo on stackblitz

Top comments (2)

Collapse
 
akkonrad profile image
akkonrad

I've got 2 general questions that actually refers to the entire article cycle.

  1. I want to avoid requests that are called multiple times from multiple components, so want to introduce an action-like (from ngrx pattern) behavior. So I thought about a FETCH-ACTION / behavior subject that is triggered on demand of data, and in case when the data is not yet fetched (or fetching), it triggers api call. Otherwise, it serves data from the store. What do you think about this approach?

  2. I also have an issue with data fetching in complex forms - for some fields I need to trigger api based on selected params in other fields. How would you approach that in the rxjs state? Should we keep those results anywhere or we can skip it and always return the latest values?

Collapse
 
ayyash profile image
Ayyash

The article is practically to create an abstract class to build different states upon request, it does not deal with interceptors nor local storage, storage is another layer you need to script. I wrote about a "cache decorator" and "localStorage wrapper" each serves a layer, in my production I put them together to form this performant enough applications that I would ship as MVPs and small to medium size applications. Not sure how far it needs enhancements for a larger scale, or what challenges you'd face there.

garage.sekrab.com/posts/localstora...
garage.sekrab.com/posts/a-cache-de...