Reputation: 1803
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
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
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
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