DEV Community

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

Posted on • Edited on

RxJS based state management in Angular - Part II

Are you keeping count? Last time I went through the basics of adding, editing and deleting from state, given that the initial list was populated from an Http Service. Today, I am diving into a specific example of continuous pagination, where the list is updated incrementally.

Challenge: appending to the current list

The list is initially populated with page 1, but on subsequent calls, we need to append, rather than set list. We start here...

// pass parameters for pagination
this.tx$ = this.txService .GetTransactions({ page: 1, size: 10 }).pipe(
   switchMap((txs) => this.txState.SetList(txs)));
Enter fullscreen mode Exit fullscreen mode

Adding the button with a simple next click for now:

<div class="txt-c">
  <button class="btn" (click)="next()">More</button>
</div>
Enter fullscreen mode Exit fullscreen mode

The next function in its simplest shape would do the following:

this is too simple but as we build the blocks we can identify which parts need rewriting

 // pagination
  next() {

    this.tx$ = this.txService.GetTransactions({ page: 2, size: 10 }).pipe(
      switchMap((txs) => {
        // append to state and return state
        this.txState.appendList(txs);
        return this.txState.stateList$;
      })
    );
  }

Enter fullscreen mode Exit fullscreen mode

So now we do not set state list, we simply append to the current state with appendList and return the actual stateList$ observable. This, as it is, believe it or not, actually works. The main tx$ observable resetting is not so cool, what is the use of an observable if I have to reset it like that, right? In addition to that, we do not want to save the current page anywhere as a static property, because we are a bit older than that, right? Now that we have a state class, why not make it richer to allow page parameters to be observables too?

Challenge: state of a single object

Let us make room for single objects in our State class. This is not the most handsome solution, nor the most robust, but it shall do for the majority of small to medium scale apps. You can create a state of either a list, or a single item, never both. In our example we need state for the pagination params.

The final product will be used like this:

   // we want to watch a new state of params and build on it
   this.tx$ = this.paramState.stateSingleItem$.pipe(
      switchMap(state => this.txService.GetTransactions(state)),
      // given that initial BehaviorSubject is set to an empty array
      // let's also change appendList to return the state observable so we can safely chain
      switchMap((txs) => this.txState.appendList(txs))
    );

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

So now we need to do two things in our state class, update the appendList to be a bit smarter (return an observable), and add a new BehaviorSubject for single item state. Let's call that stateItem$ (so creative!)

  // in state class, a new member
  protected stateItem: BehaviorSubject<T | null> = new BehaviorSubject(null);
  stateItem$: Observable<T | null> = this.stateItem.asObservable();

   appendList(items: T[]): Observable<T[]> {
    const currentList = [...this.currentList, ...items];
    this.stateList.next(currentList);
    // change to return pipeable (chained) observable
    return this.stateList$;
  }

  //  set single item state
  SetState(item: T): Observable<T | null> {
    this.stateItem.next(item);
    return this.stateItem$;
  }

  // and a getter
   get currentItem(): T | null {
    return this.stateItem.getValue();
  }
Enter fullscreen mode Exit fullscreen mode

And of course, since we have set-state, we need, update and remove state

  UpdateState(item: Partial<T>): void {
    // extend the exiting items with new props, we'll enhance this more in the future
    const newItem = { ...this.currentItem, ...item }; 
    this.stateItem.next(newItem);
  }

  RemoveState(): void {
    // simply next to null
    this.stateItem.next(null); 
  }
Enter fullscreen mode Exit fullscreen mode

Now back to our component, we need to create a new state service of "any" for now (page, and size), and inject it.

// param state service
@Injectable({ providedIn: 'root' }) // we need to talk about this later
export class ParamState extends StateService<any> {}
Enter fullscreen mode Exit fullscreen mode

In Transaction list component

constructor(
    private txState: TransactionState,
    private txService: TransactionService,
    // injecting new state
    private paramState: ParamState,
  ) {}

  ngOnInit(): void {

    this.tx$ = this.paramState.stateItem$.pipe(
      switchMap(state => this.txService.GetTransactions(state)),
      // nice work
      switchMap((txs) => this.txState.appendList(txs))
    );

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

And in transaction template, there is nothing to change. Let's now fix the next function, so that all it does, is update the param state, now that's a relief.

 next() {
    // get current page state
    const page = this.paramState.currentItem?.page || 0;
    // pump state, and watch magic
    this.paramState.UpdateState({
      page: page + 1,
    });
  }
Enter fullscreen mode Exit fullscreen mode

And because we are extending the item in the UpdateState method, to the current item, we do not have to pass all props. But that was a shallow clone, do we need to deep clone the new item? Not sure. Do you know?

Cleanup, extend, and make a mess

It is clear to us now that some functions are redundant, and some, could return observables rather than void. For example, I do not have to have a SetList, if I have an empty-list and append-list. But I don't like that. It is easier from a consumer point of view to have two distinctive methods, and on the long run, it is less error prone. We can however, reuse the SetList internally, and add an empty-list feature.

 appendList(items: T[]): Observable<T[]> {
    const currentList = this.currentList.concat(items);
    // reuse set-list
    return this.SetList(currentList);
  }

// add empty list for vanity purposes
 emptyList() {
    this.stateList.next([]);
  }
Enter fullscreen mode Exit fullscreen mode

But because we are going the "backwards" way of designing the class, I do really want to avoid a function I am not using in a component. So let's keep it down a bit, let's not return an observable, until we need one.

Next Tuesday...

We have other properties to keep track of, specifically the total count of records on the server, and also, the local param state, instead of the one provided in root. These, I will set time to write about next week. Let me know what you think about this approach.

Top comments (0)