-
Notifications
You must be signed in to change notification settings - Fork 403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discussion: Selector Composition and Query Classes #386
Comments
Query Class concept export class UserTodosQuery {
@Selector([UserState.getCurrentUserId, TodoState])
getCurrentUserTodos(userId: string, todoState: TodoStateModel) { ... }
@Selector([UserState.getCurrentUserId, TodoState.getInCompleteTodos])
getCurrentUserIncompleteTodos(userId: string, todos: Todo[]) { ... }
} You would do this where you want to combine two selectors across two states that don't have a common route. I've also seen people mention that they want this sort of thing for feature modules so that they can combine states between 2 distinct feature modules. You wouldn't want to force a dependency inside the feature module to another for the sake of a selector that you only need when you are using both feature modules. This type of class promotes decoupling from the State. The simplicity of the selectors and selector composition is very valuable when combining state across different parts of the app. Allowing for composed Selectors in non-state classes allows for a clean separation of selectors if someone prefers to keep them separate from the state as a part of their application style (command and query code separation). An argument against this would be that you could just combine the selectors by using A comment from @leon in our team discussion:
Please vote for Query Class concept or against: 👍 or 👎 |
State Class parameterless Selector @State<UserStateModel>( ... )
export class UserState {
@Selector()
getUsers(userStateModel: UserStateModel) { ... }
} Please vote 👍 or 👎 |
Query Class parameterless Selector should be invalid export class AppQuery {
@Selector() // Invalid! This should throw an error.
getSomeInfo(appStateModel: any) { ... }
} Please vote to agree or disagree that this should be seen as invalid 👍 or 👎 |
State Class Selector with parameters (Option 1) @State<UserStateModel>( ... )
export class UserState {
@Selector([UserState, Companies.getCompanies])
getUsers(userStateModel: UserStateModel, companies: Company[]) { ... }
} The motivation for dropping the first function parameter containing the state is so that the method signature for composite Selectors is consistent if they are on a State or a Query class. It would allow this selector to move to the Query class from the State class or visa versa without any modification. |
State Class Selector with parameters (Option 2) @State<UserStateModel>( ... )
export class UserState {
@Selector([Companies.getCompanies])
getUsers(userStateModel: UserStateModel, companies: Company[]) { ... }
} Please vote 👍 or 👎 |
Query Class Selector with parameters (Option 1) export class UserTodosQuery {
@Selector([UserState.getCurrentUserId, TodoState])
getCurrentUserTodos(userId: string, todoState: TodoStateModel) { ... }
} Please vote 👍 or 👎 |
Query Class Selector with parameters (Option 2) export class UserTodosQuery {
@Selector([UserState.getCurrentUserId, TodoState])
getCurrentUserTodos(appState: any, userId: string, todoState: TodoStateModel) { ... }
} I disagree with this because it promotes a special knowledge of the application state structure (for which there would have typically been no interface defined). This breaks encapsulation and would make for fragile code. |
Allowed Selector composition parameters (State Class) export class UserTodosQuery {
@Selector([UserState, TodoState])
getCurrentUserTodos(userState: UserStateModel, todoState: TodoStateModel) { ... }
} Please vote 👍 or 👎 |
Allowed Selector composition parameters (Selector) export class UserTodosQuery {
@Selector([UserState.getCurrentUserId, TodoState])
getCurrentUserTodos(userId: string, todoState: TodoStateModel) { ... }
} Please vote 👍 or 👎 |
Allowed Selector composition parameters (lambda) @State<UserStateModel>( ... )
export class UserState {
@Selector([(userState) => userState.currentUserId])
getUsers(userStateModel: UserStateModel, currentUserId: string) { ... }
} This parameter type does not really make sense to me but has been included for the sake of completeness to reflect the ways in which the |
Approach for dynamic @selector arguments That being said, we can achieve this at the moment using a pattern often used for redux selectors... @State<UserStateModel>( ... )
export class UserState {
@Selector()
getFilteredUsersFn(userStateModel: UserStateModel) {
return (filter: string) => userStateModel.users.filter((user) => user.indexOf(filter) >= 0)
}
} And then the component would contain: @Component({...})
export class AppComponent {
@Select(UserState.getFilteredUsersFn) filteredUsersFn$: Observable<(filter: string) => User[]>;
get currentFilteredUsers$() {
return this.filteredUsersFn$.pipe(map(filterFn => filterFn('myFilter')));
}
} This approach can be taken with the current version if NGXS. |
Enhanced support for dynamic @selector arguments (inner function memoization) @State<UserStateModel>( ... )
export class UserState {
@Selector()
getFilteredUsersFn(userStateModel: UserStateModel) {
return (filter: string) => userStateModel.users.filter((user) => user.indexOf(filter) >= 0)
}
} Please vote 👍 or 👎 |
Selector options export class SomeStateClassOrRegularService {
@Selector([UserState.getName, TodoState], { memoizer: customMemoizer(3) })
getUserTodos(userName: string, todoState: TodoStateModel) { ... }
} PS. selector option given as an example but I don't think we need to implement the options part until there is an actual need for it. |
Hi @leon @Dyljyn @paulstelzer (This 'Discussion' issue is a 'Monologue' until somebody else posts in it. Please help me change that 😉 ) |
Hi @markwhitfeld I hope this makes some sense. I like the approach of having Query Classes separate from the state classes. What I often do is something like Todd Moto presented at ng-conf 2018. Start with the router selector and build on top of the url parameters with many further selectors in chains. This keeps my container component very clean and the selector files get pretty big. The good thing is they are easily testable. |
Feedback: Query Class conceptI like the idea, I don't feel like it should be a decorator on a misc class though. What if we made something like redux does where its just a function and then you can make it into whatever you want? State Class parameterless SelectorI'm opposed to make a breaking change. Query Class parameterless Selector should be invalidSee State Class Selector with parameters (Option 1)I don't like this one because its just more code. Plus it deviates from the way it works now. State Class Selector with parameters (Option 2)Personally i like this one. It combines the concept we have now with the ability to add N* selectors to it. The code looks quite nice in real world too... Here is a real world case I'm using it. @Selector([PreferencesState.grid, PreferencesState.user])
static items(state: ItemsStateModel, { sort }, { nameSortOrder, sizeSortOrder, hideAnnoyingFiles }) {
const items = hideAnnoyingFiles ?
state.items.filter(i => i.name[0] !== '.') :
[...state.items];
return items.sort((a: any, b: any) =>
sort.direction === 'asc' ?
sortItems(a, b, sort, nameSortOrder, sizeSortOrder) :
sortItems(b, a, sort, nameSortOrder, sizeSortOrder)
);
} Query Class Selector with parameters (Option 1)See thoughts on Query Class Selector with parameters (Option 2)Not a fan. Allowed Selector composition parameters (State Class)Ya I'd expect this. Allowed Selector composition parameters (lambda)I don't mind this. Can't think of a good idea to use it but hey. Approach for dynamic @selector argumentsI was actually thinking the same thing for this. Enhanced support for dynamic @selector arguments (inner function memoization)Ya i'd expect this. Selector optionsI had this idea when i made this feature but never had a great use case for it so i didn't see the point. |
Thinking about this some more, here is a use case I can think of:
After thinking about that, I have these questions/thoughts:
|
I can say, I'm on board with the following:
The query class idea just feels like a bad idea to me though. |
I think that now that selector functions work as expected, calling them that parent classes is enough. You can define selectors in child states and then call those from the parent. then combining selectors is just function composition. If we add a special synax for composing I am afraid we are adding unneeded complexity. But I certainly seem to be in the minority in this regard. My personal feeling is that as long as we provide the ABILITY to combine selectors function (which previously didn't work) that should be where the library's responsibility ends |
I like the query class concept, as I'm all for refactoring the complexity away from the components, but without handing all of the responsibility to ngxs. The one idea that stands out is |
This is almost 100%, still need ability to do pass arguments. @markwhitfeld - can u look at that? |
@amcdnl I'll do that one |
Hi, there is a problem with the current design of the library that should be addressed in future releases. Let's say there is a state that consist of 3 properties. @State({
name: 'foo',
defaults: {
a: 0,
b: 0,
c: 0,
}
})
export class FooState {
// actions
@Selector()
static selector(state) {
return { a: state.a, b: state.b };
}
} The problem with this code is that whenever Since it would a breaking change that will have to wait for NGXS 4, we could potentially make another decorator such as Finally, as a bonus, something along the lines of reselect's |
@maxencefrenette - can you open a new issue to discuss this? |
Discussion
The purpose of this issue is to facilitate a discussion around the possibilities for selectors and querying the application state. Many ideas on this topic will be listed here.
Versions
Items to agree on:
@Selector(...)
method signatures@Selector(...)
method signaturesI would like to try some rules to help facilitate voting on ideas ( because... why not! ):
The text was updated successfully, but these errors were encountered: