-
Notifications
You must be signed in to change notification settings - Fork 404
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
🚀[FEATURE]: Provide state slices as selectors out-of-the-box #1653
Comments
last solution can also be
|
Hi @Carniatto, @joaqcid, Take a look at this playground. When every state has its own slicer, you no longer have a slicing problem. ;) Have a cheerful day. |
that's another interesting way of doing it, we thought of using |
I prefer functions, because they are more flexible and reusable. That doesn't mean keys can't be used. I'd rather not have an overloaded function though. Please consider introducing separate functions, if you decide having both. |
I think the function defeats the purpose of making it more readable, but maybe we can consider accepting both? |
@markwhitfeld @Carniatto what do you think about: import { sliceByState } from '@ngxs/store/utils';
@Selector([ sliceByState({ state: AppState, path: 'users' }) ])
static getUsers.... |
i think the one i like the most is this one
|
Similarly I've used dynamic properties to select correct slice of state where decorators are not allowed. interface AppStateModel{
state1: number
state2: string
}
export class AppStateSelectors {
static sliceOf<K extends keyof AppStateModel>(stateKey: K, args?: any) {
return createSelector([AppState], (state: AppStateModel) => {
return state[stateKey] as AppStateModel[K] // by doing so, we can have its type when selecting.
})
}
// to use
stuffsToDo(){
this.store.select(AppStateSelectors .sliceOf('state1')) // returns correct type as number
this.store.select(AppStateSelectors .sliceOf('state2')) // returns correct type as string
this.store.select(AppStateSelectors .sliceOf('state100')) // oops, error, key not found
// other stuffs
} |
I came up with a similar solution as mentioned in #1653 (comment):
To be used like:
It's a bit more generic and requires a state token but you do get strong typing and intellisense down stream. |
Here is a initial proposal for this feature: import { createSelector, StateToken } from '@ngxs/store';
import { StateClass } from '@ngxs/store/internals';
type SelectorOnly<TModel> = StateToken<TModel> | ((...arg: any) => TModel);
type Selector<TModel> = StateClass<any> | SelectorOnly<TModel>;
export type PropertySelectors<TModel> = {
[P in keyof TModel]: (model: TModel) => TModel[P];
};
export function createPropertySelectors<TModel>(
state: Selector<TModel>,
): PropertySelectors<TModel> {
const cache: Partial<PropertySelectors<TModel>> = {};
return new Proxy(
{},
{
get(target: any, prop: string) {
const selector = cache[prop] || createSelector([state], (s: TModel) => s?.[prop]);
cache[prop] = selector;
return selector;
},
},
);
}
interface SelectorMap {
[key: string]: SelectorOnly<any>;
}
type MappedSelector<T extends SelectorMap> = (...args: any[]) => MappedResult<T>;
type MappedResult<TSelectorMap> = {
[P in keyof TSelectorMap]: TSelectorMap[P] extends SelectorOnly<infer R> ? R : never;
};
export function createMappedSelector<T extends SelectorMap>(selectorMap: T): MappedSelector<T> {
const selectors = Object.values(selectorMap);
return createSelector(selectors, (...args) => {
return Object.keys(selectorMap).reduce((obj, key, index) => {
(obj as any)[key] = args[index];
return obj;
}, {} as MappedResult<T>);
}) as MappedSelector<T>;
}
export function createPickSelector<TModel, Key extends keyof TModel>(
state: Selector<TModel>,
keys: Key[]
) {
const selectors = keys.map((key) => createSelector([state], (s: TModel) => s[key]));
return createSelector([...selectors], (...selected: any) => {
return keys.reduce((acc, key, index) => {
acc[key] = selected[index];
return acc;
}, {} as Pick<TModel, Key>);
});
} And here some examples of usage: /* These are some example usages */
import { createPropertySelectors, createMappedSelector } from "./ngxs-next";
interface AppStateModel {
firstName: string,
lastName: string
}
interface AuthStateModel {
logged: boolean,
grants: {
read: boolean,
write: boolean,
delete: boolean
}
}
// app.state.selectors.ts
export class AppStateSelectors {
/* Case1: Creating base props selectors from a state */
static props = createPropertySelectors<AppStateModel>(AppState);
@Selector([AppStateSelectors.props.firstName, AppStateSelectors.props.lastName])
welcomeMsg(fistName: string, lastName: string) {
return `Welccome ${firstName} ${lastName};
}
}
// auth.state.selectors.ts
export class AuthStateSelectors {
static props = createPropertySelectors<AuthStateModel>(AuthState);
}
/* Case2: Creating a mapped selector from selectors */
const authUser = createMappedSelector({
firstName: AppStateSelectors.props.firstName,
logged: AuthStateSelectors.props.logged
})
/* Case3: Creating a selector by pick properties */
const readWriteGrants = createPickSelector(AuthStateSelectors.props.grants, ['read', 'write'])
const adminGrants = createPickSelector(AuthStateSelectors.props.grants, ['read', 'write', 'delete']) I've also created this Gist here where is possible to copy Please try it out. We are looking for feedback to refine this before integrating out of the box |
@Carniatto Could you improve your |
@markwhitfeld That seems like a reasonable update and would reduce chances of breaking changes in the future if the signature does need to change 👍 |
@markwhitfeld @rbudnar I've updated the Gist and the comment to match your suggestions. |
Hi @Carniatto, How do we select interface SomeStateModel {
foo: {
bar: {
baz: string;
};
};
} Please take a look at the updated playground. I believe you will find it valuable. |
@armanozak @Carniatto I left the same feedback/question in the slack channel. I looked at possibly using something like lodash's get method to possibly selected deeply nested properties. Not sure how to implement it though. |
@armanozak I think I figured it out. The idea is to just keep creating selectors from the property selectors initially made. It's working really well for me so far. // This is from the final proposal
static props = createPropertySelectors<Model>(AppState);
// Keep creating selectors
static fooProps = createPropertySelectors(AppState.props.foo);
static barProps = createPropertySelectors(AppState.fooProps.bar); |
This does not look practical. It is a lot of work for not so much of a gain. Besides, you will end up creating selectors that you will never use anywhere (other than within the same state class). |
I believe creating selectors that are only used in the state class is not a bad practice. Matter of fact I'd encourage using this to promote smaller and easier to test selectors. About the necessity to deeply create slices, I believe that's not the purpose of these helpers and it can be achieved using |
@armanozak It's funny to me, but I understand the desire to reduce the number of selectors and to only have as many as you may need, and by using this approach I went from 14 selectors (all of them used) to 4 selectors. before public static getSlice(property: keyof RaterQuoteFormStateModel) {
return createSelector([RATER_QUOTE_FORM_STATE_TOKEN], (state: RaterQuoteFormStateModel) => state[property]);
}
@Selector([RaterQuoteFormState.getSlice('form')])
private static _getYear(form: RaterQuoteFormModel) {
return form.year;
}
@Selector([RaterQuoteFormState.getSlice('form')])
private static _getDealerCode(form: RaterQuoteFormModel) {
return form.dealerCode;
}
@Selector([RaterQuoteFormState._getDealerCode])
public static getDealerCode(dealerCode: string) {
return dealerCode;
}
@Selector([RaterQuoteFormState.getSlice('errors')])
public static getQuoteResponseErrors(errors: RaterQuoteFormErrors) {
return errors.quoteResponse;
}
@Selector([RaterQuoteFormState._getYear])
public static getYear(year: string) {
return year;
}
@Selector([RaterQuoteFormState.getSlice('form')])
private static _getMake(form: RaterQuoteFormModel) {
return form.make;
}
@Selector([RaterQuoteFormState._getMake])
public static getMake(make: string) {
return make;
}
@Selector([RaterQuoteFormState.getSlice('form')])
private static _getVin(form: RaterQuoteFormModel) {
return form.vin;
}
@Selector([RaterQuoteFormState._getVin])
public static getVin(vin: string) {
return vin;
}
@Selector([RaterQuoteFormState.getSlice('form')])
public static getMileage(form: RaterQuoteFormModel) {
return form.mileage;
}
@Selector([RaterQuoteFormState.getSlice('form')])
private static _getModel(form: RaterQuoteFormModel) {
return form.model;
}
@Selector([RaterQuoteFormState._getModel])
public static getModel(model: string) {
return model;
}
@Selector([
RaterQuoteFormState.getVin,
RaterQuoteFormState.getYear,
RaterQuoteFormState.getMake,
RaterQuoteFormState.getModel,
])
public static getVehicleFormInfo(vin: string, year: string, make: string, model: string) {
return { vin, year, make, model };
} after public static props = createPropertySelectors(RATER_QUOTE_FORM_STATE_TOKEN);
public static formProps = createPropertySelectors(RaterQuoteFormState.props.form) as PropertySelectors<RaterQuoteFormModel>;
public static errorProps = createPropertySelectors(RaterQuoteFormState.props.errors) as PropertySelectors<RaterQuoteFormErrors>;
@Selector([
RaterQuoteFormState.formProps.vin,
RaterQuoteFormState.formProps.year,
RaterQuoteFormState.formProps.make,
RaterQuoteFormState.formProps.model,
])
public static getVehicleFormInfo(vin: string, year: string, make: string, model: string) {
return { vin, year, make, model };
} So, while it may seem at first like As a side note, the reason I have private selectors is that it is my attempt at creating an optimized selector: // this is not optimized enough for me... I don't want this selector to evaluate anytime the form changes
@Selector([RaterQuoteFormState.getSlice('form')])
private static _getMake(form: RaterQuoteFormModel) {
return form.make;
}
// this will only evaluate if make has changed, not the entire form.
@Selector([RaterQuoteFormState._getMake])
public static getMake(make: string) {
return make;
} So far I have noticed that the potentially new property selectors are optimized, so that is another reason to continue to try this new approach. |
Well, everything “can be achieved using Never mind. You have asked about my opinion and here it is: Selecting only the first level of properties is definitely not worth discussing. So, please proceed as you prefer. |
I'll just add my 2c here 😉 I think that the topic of deep selectors is a separate one to what is being proposed here. I hope that this makes sense and thank you so much for all the feedback and ideas!!! |
@richarddavenport Just a further optimisation you could make to your code with the other proposed selectors: public static props = createPropertySelectors(RATER_QUOTE_FORM_STATE_TOKEN);
public static formProps = createPropertySelectors(RaterQuoteFormState.props.form) as PropertySelectors<RaterQuoteFormModel>;
public static errorProps = createPropertySelectors(RaterQuoteFormState.props.errors) as PropertySelectors<RaterQuoteFormErrors>;
@Selector([
RaterQuoteFormState.formProps.vin,
RaterQuoteFormState.formProps.year,
RaterQuoteFormState.formProps.make,
RaterQuoteFormState.formProps.model,
])
public static getVehicleFormInfo(vin: string, year: string, make: string, model: string) {
return { vin, year, make, model };
} After (using public static props = createPropertySelectors(RATER_QUOTE_FORM_STATE_TOKEN);
public static formProps = createPropertySelectors(RaterQuoteFormState.props.form) as PropertySelectors<RaterQuoteFormModel>;
public static errorProps = createPropertySelectors(RaterQuoteFormState.props.errors) as PropertySelectors<RaterQuoteFormErrors>;
public static getVehicleFormInfo = createMappedSelector({
vin: RaterQuoteFormState.formProps.vin,
year: RaterQuoteFormState.formProps.year,
make: RaterQuoteFormState.formProps.make,
model: RaterQuoteFormState.formProps.model
}); After (using public static props = createPropertySelectors(RATER_QUOTE_FORM_STATE_TOKEN);
public static formProps = createPropertySelectors(RaterQuoteFormState.props.form) as PropertySelectors<RaterQuoteFormModel>;
public static errorProps = createPropertySelectors(RaterQuoteFormState.props.errors) as PropertySelectors<RaterQuoteFormErrors>;
// note that this doesn't even make use of your formProps selector, and achieves the same optimised selector!
public static getVehicleFormInfo = createPickSelector(RaterQuoteFormState.props.form, ['vin', 'year', 'make', 'model']); Thanks for the example to try this out on! |
These are finally landing in the next release! |
Great news! v3.8.0 has been released and it includes a fix for this issue. |
Relevant Package
This feature request is for @ngxs/store
Description
Provide state slices as selectors out-of-the-box
Describe the problem you are trying to solve
Please describe the problem. If possible please substantiate it with the use cases you want to address.When working with
ngxs
selectors is common t have basic selectors just to slice the basic properties of your state like shown below:This can become a tedious process. There is a possibility to improve this using
dynamic selectors
:This is a better approach but been a very common use case would be nice to have something like this out-of-the-box.
Describe the solution you'd like
A possible solution would be to provide a function
createPropertySelectors
that would split the state in selectors or each property like shown below:Describe alternatives you've considered
There are other alternatives provided by @poloagustin and @joaqcid respectivly
The text was updated successfully, but these errors were encountered: