Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ngrx - createSelector vs Observable.combineLatest

I just ran into the custom selectors of @ngrx and I simply cannot be amazed by the feature.

Following their use case of books for the selectedUser, I can't give a real good reason to use a custom selector such as :

export const selectVisibleBooks = createSelector(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
    return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});

instead of something like :

export const selectVisibleBooks = Observable.combineLatest(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
    return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});

I tried to convince myself that the memoization of the createSelector is the crucial part, but as far as I understood, it cannot perform these performance boosts to non-primitive values, so it wont really save any computing for non primitive slices, which by using Rx's distinctUntilChanged operator with the combineLatest can be solved.

So what have I missed, why should I use @ngrx/selector?

Thanks in advance for any insights.

like image 630
Kesem David Avatar asked Jan 10 '18 12:01

Kesem David


People also ask

Are NGRX selectors Memoized?

Benefits of using selectors There is a number of benefits: Memoization. First of all, selectors created by createSelector are memoized, meaning that they won't be called unnecessarily unless the data in the store has changed.

Are selectors observables?

Selectors are pure functions not observables.

What is createFeatureSelector?

The createFeatureSelector is a convenience method for returning a top level feature state. It returns a typed selector function for a feature slice of state.


1 Answers

Maybe there is more to it than memoization but I didn't see anything that stood out in the source code. All that is advertised in the docs is memoization and a way to reset it which you can basically do with a distinct operator as well. I'd say that the reason to use it is that it is convenient. At least in simple cases it is more convenient than strapping distinct operators onto every input to the combineLatest.

Another benefit is that it allows you to centralize the logic related to the internal structure of your state. Instead of doing store.select(x => foo.bar.baz) everywhere, you can create a selector for it and do store.select(selectBaz). You can combine selectors to. In this way you should only have to setup the logic to traverse the state tree in one place. This is beneficial if you ever have to change the structure of your state since you will only have to make the change in one place rather than finding every selector. Everyone may not like the addition of creating more boilerplate though. But as someone who had to do a major refactor of state, I only use selectors.

createSelector is pretty basic though so you can only use it for basic sorts of operations. It falls short in scenarios where you are retrieving lists of objects for which you only need a filtered subset. Here is an example:

const selectParentVmById = (id: string) => createSelector<RootState, Parent, Child[], ParentVm>(
    selectParentById(id),
    selectChildren(),
    (parent: Parent, children: Child[]) => (<ParentVm>{
        ...parent,
        children: children.filter(child => parent.children.includes(child.id))
    })
);

In this scenario the selector selectParentVmById will emit when selectChildren() emits a different array which happens if any of the elements inside of it has changed. This is great if the element that changed is one of the parent's children. If it isn't then you get needless churn because the memoization is done on the whole list rather than the filtered list (or rather the elements inside of it). I have a lot of scenarios like this and have started only using createSelector for simple selectors and combining them with combineLatest and rolling my own memoization.

This isn't a reason to not use it in general, you just need to know its limitations.

Extra Credit

Your question wasn't about this but since I brought up the problem I figured I'd give the solution for completeness. I started using a custom operator named distinctElements() that would act like distinctUntilChanged() but apply to the elements in a list rather than the list itself.

Here is the operator:

import { Observable } from 'rxjs/Observable';
import { startWith, pairwise, filter, map } from 'rxjs/operators';

export const distinctElements = () => <T extends Array<V>, V>(source: Observable<T>) => {
    return source.pipe(
        startWith(<T>null),
        pairwise(),
        filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
        map(([a, b]) => b)
    )
};

Here would be the above code refactored to use it:

const selectParentVmById = (store: Store<RootState>, id: string): ParentVm => {
    return store.select(selectParentById(id)).pipe(
        distinctUntilChanged(),
        switchMap((parent) => store.select(selectChildren()).pipe(
            map((children) => children.filter(child => parent.children.includes(child.id))),
            distinctElements(),
            map((children) => <ParentVm> { ...parent, children })
        ))
    );
}

Takes a bit more code but it cuts out the wasted work. You could add a shareReplay(1) depending on your scenario.

like image 193
bygrace Avatar answered Oct 14 '22 07:10

bygrace