platform
platform copied to clipboard
ComponentStore: Allow multiple selector functions to store.select
Currently, store.select can either be passed a selector function, or alternately multiple selectors can be combined by passing 2 or more selectors with a project function. It would be convenient if each of the "combine selectors" could accept either a selector or a function, in order to concisely select multiple properties from the state that don't already have selectors defined:
this.store.select(
state => state.foo,
state => state.bar,
(foo, bar) => ({ foo, bar })
);
Basically, if the select function encounters a function as one of the arguments instead of an observable, all it would have to do is wrap the function internally with this.select(selectorFn)
Signature for the overload for two selectors would be something like
select<R, S1, S2>(
s1: Observable<S1> | (s: T) => S1,
s2: Observable<S2> | (s: T) => S2,
projector: (s1: S1, s2: S2) => R,
config?: SelectConfig
): Observable<R>;
Describe any alternatives/workarounds you're currently using
this.store.select(
this.store.select(state => state.foo),
this.store.select(state => state.bar),
(foo, bar) => ({ foo, bar })
);
Note that @ngrx/store supports this.
from https://ngrx.io/guide/store/selectors#using-selectors-for-multiple-pieces-of-state:
export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;
export const selectVisibleBooks = createSelector(
selectUser,
selectAllBooks,
(selectedUser: User, allBooks: Book[]) => {
if (selectedUser && allBooks) {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
} else {
return allBooks;
}
}
);
Since this is the state of a component, I'm not sure if we need this. What would be the benefit of this, in comparison to simply selecting those props from the state?
vm$ = this.store.select(state => ({ foo: state.foo, bar: state.bar }))
What would be the benefit of this, in comparison to simply selecting those props from the state?
vm$ = this.store.select(state => ({ foo: state.foo, bar: state.bar }))
This will emit a new value every time the state changes, even if foo and bar haven't changed. The selector creates a new object, so the internal call to distinctUntilChanged emits every time.
Take a look here to see this in action: https://stackblitz.com/edit/ngrx-component-store-selectors?file=src/app/app.component.ts&devtoolsheight=50 (observe the console to see logging output. Selector A is output multiple times, but selector B is only output once)
This actually could be documented better to ensure other devs understand the difference.
@timdeschryver @alex-okrushko I can take this one
IIRC, we discussed this when the implementation was on going, but I can't find it anymore... I think we decided not to do it because it shouldn't be a big performance hit in combination that it would require more code (and we would want to use the existing selectors).
Generally, I like the idea. @timdeschryver @markostanimirovic maybe we can explore it past v11?
Sounds good to me 👍
I can look at this. I'm thinking the simplest implementation is creating a selector under-the-hood when multiple selectors are passed to select:
// the return type would be inferred normally
const selections: Observable<[ResultOfSelectorOne, ResultOfSelectorTwo]> = this.store.select(selectorOne, selectorTwo);
// in `select` method
const combinedSelector = args.length > 1 ? createSelector(...args, (...args) => (...slices) => slices) : args[0];
I disagree with adding this as I think it muddies up the separation between selectors and the store
Ah, fair point
This could be implemented without using createSelector so the divisions could remain in place.
Since this is already supported, I can see why store.select doesn't need this feature:
store.select(createSelector(
selectOneThing,
selectAnotherThing,
selectAThirdThing
)
);
However, this requires a dependence on createSelector.
This anonymous method is also harder to unit test, so it's likely that the anonymous selector should be extracted into its own named selector.
But, that creates more boilerplate.
My assessment is:
Pros to store.select supporting multiple selectors
- Natural API for getting multiple values
- Developer gets free performance benefits
- Avoids pitfalls like a developer using combineLatest with multiple store.selects where they may experience issues with first emissions
- Reduces boilerplate of one-time selectors
Cons
- New API (increases complexity of project) for something technical achievable already
- There is a trivial addition of time added to check for distinct changes compared to only having to use ===
To be clear, I am not recommending the API from the original issue post, which does look like the createSelector api with a series of selectors and a projector.
I'm suggesting select is able to take in a variadic number of map functions only.
export function select<T, Results extends unknown[]>(
...mapFns: [...{ [i in keyof Results]: (state: T) => Results[i] }]
): (source$: Observable<T>) => Observable<Results>;
Here's another possible solution and convenience.
Today, components may select many values like so:
this.valueOne$ = this.store.select(selectValueOne);
this.valueTwo$ = this.store.select(selectValueTwo);
this.valueThree$ = this.store.select(selectValueThree);
this.valueFour$ = this.store.select(selectValueFour);
This could be simplified:
this.data = this.store.selectMany({
valueOne: selectValueOne,
valueTwo: selectValueTwo,
valueThree: selectValueThree,
valueFour: selectValueFour
});
In the template, the result could be used:
value three is: {{ data.valueThree$ | async }}
This proposed API for a "selectMany" function:
- creates independent observables for each piece of data, keeping them decoupled
- reduces boilerplate (reduces variable declarations and invocations of
select)
Honestly, the suggested selectMany above doesn't look any better than having 4 individual observables.
What would be more helpful though, is creating a single observable view model from that:
this.vm$ = this.store.selectMany({
valueOne: selectValueOne,
valueTwo: selectValueTwo,
valueThree: selectValueThree,
valueFour: selectValueFour
});
// and then in the template
<ng-container *ngrxLet="vm$ as vm">
One: {{ vm.valueOne }}
Honestly, the suggested selectMany above doesn't look any better than having 4 individual observables.
What would be more helpful though, is creating a single observable view model from that:
Your suggested API is nice for creating a single observable. My intention for recommending separate streams was for potentially extreme/unnecessary performance concerns. Whenever any of the values changes, a new "view model" object will be created (presumably from using combineLatest), potentially causing more areas of the template to be re-evaluated than needed.