SurfMan
SurfMan

Reputation: 1803

Change parameter without changing the subscription

My Angular component can use a query parameter status to filter the displayed results. It will go into the store using a selector and that Observable this.items$ is then kept in the component.

This is the code I have currently written:

this.route.queryParams.pipe(
      takeUntil(this.destroy$)
    ).subscribe((params: Params) => {
      const status = params['status'];
      if (status) {
        this.items$ = this.store.select(selectItemsByStatus, {status: status});
      }
      else {
        this.items$ = this.store.select(selectItemsByStatus, {status: 'all'});
      }
    });

Even though this works, my feeling says that this can be done differently, more logically, more reactive if you will. I don't think I need to do the select over and over again. Is there a way to keep ONE select that will emit different values when the status changes?

Upvotes: 1

Views: 788

Answers (3)

deaks
deaks

Reputation: 315

Have you considered @ngrx/router-store? It might be overkill for what you need, but if you are accessing the router state a lot it could be worth your while.

You could defer most of the logic to a selector like so:

export const selectVisibleItems = createSelector(
    selectState,
    selectQueryParams, // this is provided by @ngrx/router-store
    (state, params) => 
         // Do your selecting of data here depending on the query params - just making something up here for an example
         state?.items?.filter(
             item => item?.status === (params?.['status'] ?? 'all')
)

Then in your component, you don't even have to bother with passing props to your selectors, which as you correctly state are deprecated.

items$ = this.store.select(selectVisibleItems);

I always try to put as much logic into the selectors as possible. From the ngrx site:

When using the createSelector and createFeatureSelector functions @ngrx/store keeps track of the latest arguments in which your selector function was invoked. Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking your selector function. This can provide performance benefits, particularly with selectors that perform expensive computation. This practice is known as memoization.

For this reason, I generally avoid using operators like combineLatest when I can compose the same result using selectors.

Upvotes: 0

olivarra1
olivarra1

Reputation: 3399

My feeling says that this can be done differently, more logically, more reactive if you will

You are absolutely right on this - When building an observable you usually need to think in the opposite direction: Instead of "this thing has changed, I need to set this value" is "This value, what does it depend on?"

In your case, it seems like items$ depends only on the route's queryParams, so we can build a stream like this:

items$ = this.route.queryParams.pipe(
  switchMap(params => {
    const status = params['status'];
    if (status) {
      return this.store.select(selectItemsByStatus, {status: status});
    }
    else {
      return this.store.select(selectItemsByStatus, {status: 'all'});
    }
  })
);

This also has the advantage that you don't need to deal with another subscription - So I think the takeUntil would be redundant here, it's up to the consumer of this stream to unsubscribe when needed.

If items$ depends on more things, you can combine other observables with combineLatest, merge, concat, etc.

Edit: Note that this will call this.store.select every time queryParams emits something, even if the status doesn't change. But there's an easy fix for this:

items$ = this.route.queryParams.pipe(
  map(params => params['status']),
  // Only emit if the value (status) changes
  distinctUntilChanged(),
  switchMap(status => {
    if (status) {
      return this.store.select(selectItemsByStatus, {status: status});
    }
    else {
      return this.store.select(selectItemsByStatus, {status: 'all'});
    }
  })
);

Upvotes: 1

martin
martin

Reputation: 96889

You can use combineLatest() with two parameters:

statusFilter$ = new BehaviorSubject('');

items$ = combineLatest(
  this.store.select(selectItems),
  statusFilter$,
).pipe(
  map(([items, statusFilter]) => items.filter(item => item.status === statusFilter)), // ... or whatever
);

Then changing the status filter is with statusFilter$.next('status'). Maybe in your case you could just subscribe the query parameter directly into the filter (but this will also propagate complete notifications so maybe this is not exactly what you want):

this.route.queryParams.pipe(...).subscribe(statusFilter$);

Upvotes: 0

Related Questions