ngrx - createSelector vs Observable.combineLatest
Asked Answered
D

1

11

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.

Documentation answered 10/1, 2018 at 12:25 Comment(2)
personally I'd rather stick to as much pure rxjs so I opt to use combineLatest and not createSelector, this way I know what's going on closer to the metal.Full
@Full after all the time past, i figured i love that approach, just like you said - more controlDocumentation
U
8

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.

Unceasing answered 16/3, 2018 at 21:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.