DEV Community

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

Posted on • Originally published at garage.sekrab.com

RxJS based state management in Angular - Part V

I should call it a quit now. One more thing to experiment with. Couple of weeks ago I came down to making a state service of IList and found out that we recreated all functionalities just to accommodate the sub property of matches and total. Today, I am going to make that part of the state class. It will prove a failure if using it for a simple array with no pagination proved to be unnecessarily complicated.

Moving the total and hasMore to the list state

We begin at the end. The Transactions observable is now responsible for total and hasMore props, thus no need to watch Params in the template.

  <!-- Watching the main observer at higher level -->
    <ng-container *ngIf="nTx$ | async as txs">
        <div class="bthin spaced">
            // it should contain its own total
            Total {{  txs.total }} items
        </div>
        <ul class="rowlist spaced">
            // and the matches are the iterable prop
            <li *ngFor="let tx of txs.matches;">
                <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>

        <button class="btn" (click)="add()">Add new</button> 
         // and the hasMore is part of it too
        <div class="txt-c" *ngIf="txs.hasMore">
            <button class="btn" (click)="next()">More</button>
        </div>
    </ng-container>
Enter fullscreen mode Exit fullscreen mode

In the component

    ngOnInit(): void {

       // back to nTx ;)
        this.nTx$ = this.paramState.stateItem$.pipe(
            distinctUntilKeyChanged('page'),
            switchMap((state) => this.txService.GetTransactions(state)),
            switchMap((txs) => {
                // calculating hasMore from param state
                const _hasMore = hasMore(txs.total, this.paramState.currentItem.size, this.paramState.currentItem.page);
               // Now this, is new, it should set list and append new
               return this.txState.appendList({...txs, hasMore: _hasMore})}),
        }

        // empty list everytime we visit this page
        this.txState.emptyList(); 

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

The first simplification we face: total now is being taken care of inside the state class

  // the add function now is slightly reduced
    add(): void {
        this.txService.CreateTransaction(newSample()).subscribe({
            next: (newTx) => {
                // no need to update param state, simply add item, it should take care of total
                this.txState.addItem(newTx);
            }
        });
    }

    delete(tx: ITransaction): void {
        this.txService.DeleteTransaction(tx).subscribe({
            next: () => {
                // this should now take care of total
                this.txState.removeItem(tx);
            }
        });
    }
Enter fullscreen mode Exit fullscreen mode

The state class then looks like this (notice how heavier it looks than original, that should be a downside)

// First lets change the IState model to IListItem
export interface IListItem {
    id: string;
}
// and let me create an IList model to hold matches array, total and hasMore
export interface IList<T extends IListItem> {
    total: number;
    matches: T[];
    hasMore?: boolean;
}

// then our ListStateService would assume an observable of the IList, rather than an array
export class ListStateService<T extends IListItem>  {
    // instantiate with empty array and total 0
    protected stateList: BehaviorSubject<IList<T>> = new BehaviorSubject({ matches: [], total: 0 });
    stateList$: Observable<IList<T>> = this.stateList.asObservable();

   // the getter
    get currentList(): IList<T> {
        return this.stateList.getValue();
    }

    // the append list should now set and append list and return an observable of IList
    appendList(list: IList<T>): Observable<IList<T>> {
        // append to internal matches array
        const newMatches = [...this.currentList.matches, ...list.matches];

       //aaargh! progress current state, with the incoming list then return
        this.stateList.next({ ...this.currentList, ...list, matches: newMatches });
        return this.stateList$;
    }

    // new: empty initial state list and total
    emptyList() {
        this.stateList.next({ matches: [], total: 0 });
    }

     addItem(item: T): void {
        this.stateList.next({
            // always must carry forward the current state 
            ...this.currentList,
            matches: [...this.currentList.matches, item],
            // update total
            total: this.currentList.total + 1
        });
     }

    editItem(item: T): void {
        const currentMatches = [...this.currentList.matches];
        const index = currentMatches.findIndex(n => n.id === item.id);
        if (index > -1) {
            currentMatches[index] = clone(item);
            // again, need to carry forward the current state
            this.stateList.next({ ...this.currentList, matches: currentMatches });
        }
    }

    removeItem(item: T): void {
        this.stateList.next({
           // and carry forward the current state
            ...this.currentList,
            matches: this.currentList.matches.filter(n => n.id !== item.id),
           // update total
            total: this.currentList.total - 1
        });
    }
Enter fullscreen mode Exit fullscreen mode

The first issue is to set the initial state with empty array, and zero matches. That is fixed with the new method emptyList().

The second issue is that since we have to take care of the object and the array, we need to carry forward the current state props in every operation. So it is like two in one! One dream, twice as many nightmares! It is not a big deal but when you start getting bugs you always question that part first.

Now to the test. Let's setup a component that gets an array of categories, with an add feature.

// the final result should look like this
<ng-container *ngIf="cats$ | async as cats">
    <ul *ngFor="let item of cats.matches">
        <li>
            {{ item.name }}
        </li>
    </ul>
    <div>
        <button class="btn-rev" (click)="add()">Add category</button>
    </div>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

Setting up the category state, and model:

export interface ICat {
    name: string;
    id: string; // required
}

@Injectable({ providedIn: 'root' })
export class CatState extends ListStateService<ICat> {
}

Enter fullscreen mode Exit fullscreen mode

Also create a service to get categories and add category. The service should return an array of categories, not a list (no matches, and total props included). For brevity, I will leave that part out.

In our component

    cats$: Observable<IList<ICat>>;

    constructor(private catService: CatService, private catState: CatState) {
        // imagine CatService :)
    }
    ngOnInit(): void {

        this.cats$ = this.catService.GetCats().pipe(
            // here goes: to appendList, we have to wrap in a proper IList<ICat> model
            switchMap((data) => this.catState.appendList({matches: data, total: data.length}))
        );

    }

    add() {
        // add dummy cat without service to prove a point
        const d = {name: 'new category', id: uuid()};

        // dummy add
        this.catState.addItem(d)

    }
Enter fullscreen mode Exit fullscreen mode

Running this works fine. So the only added complexity is having to wrap the returned array in a pseudo model with matches property, and a useless total property.

Side effects

So doing a sub array added complexity in the state itself, and made us aware of the IList model where it is not needed. Though the complexity is not huge, and for most of the Get List operations that usually are paginated, it should be a benefit, I... however... dislike it. For two reasons:

  • Wrapping the returned array in a model of no use seems too contrived
  • Open wound, the list state class has a lot of wounds that could easily be infected and eventually blow up in our faces.

Final verdict

To live true to our target of simplicity, I removed the IList implementation. Find the final state service on Stackblitz. Please do let me know if something was not clear, or was buggy and overlooked, or you have a better (simpler) idea. Thanks for coming this far, and to reward you for your patience, here is a joke:

And the bartender says, "Success, but you're not ready!"

So a JavaScript function walks into a bar.

Thanks 🙂

Resources:

Top comments (0)