-
Notifications
You must be signed in to change notification settings - Fork 12
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
Question regarding custom reducers. #56
Comments
Hi @mxdmedia. Apologies for the lax documentation, this is something I am working on. Regarding custom reducers, yes, you can absolutely write your own! Auto-Entity is just built on top of NgRx, and the way it was designed, it generally does not change how you normally use NgRx. So you can always do the same kinds of things you might normally do, such as write reducers. The empty stub reducers Auto-Entity requires can easily have custom cases added to them if you need to. Your reducers, stub or otherwise, are included in your state as you would normally include any other reducer, with the ActionReducerMap. So they are there, even though...normally...they are just the stubs. All you would really need to do is add a switch statement and go from there. (It should also be possible to write a reducer using the new NgRx 8 patterns as well, again...Auto-Entity does not change anything here, it goes with the standard flow, as it were, for however NgRx normally works.) Now, as to the specifics of reducing your data structure into Auto-Entity managed state...that should also be possible. There are probably a few ways to achieve it. You could write your own reducer, but there is also the option to create a custom action to INITIATE the process, then write an effect that handles that action, calls a custom service method to get your "bundle" of entities, then in return dispatch a number of other actions...Auto-Entity generic actions...to integrate your custom initiation with Auto-Entity Success/Failure actions, which will be properly reduced by Auto-Entity's internal meta reducer. I can give you more details once I get a chance to think about it a bit more tomorrow. |
Thanks for the response @jrista . I am going to use auto-entity on a test project today, and see how far I can get. I look forward to hearing your further thoughts/details. One thing to clarify. First you say:
then
I assume that these are not the same, due to the The custom actions, and handling it from that direction seems promising. |
@mxdmedia So, we have done what we can to make auto-entity state compatible with @ngrx/entity. We have not done a lot of testing, however there is nothing that says you shouldn't be able to create an entity adapter and use that to update state that auto-entity manages. That said, the easier way to handle it all is to create an effect that handles your initiating action, and then dispatch multiple auto-entity generic actions that auto-entity's meta reducer will handle to update the state for you. Much simpler that way. So, to give you some actual code... You basically have an import { Class, Order, Family, Genus, Species } from 'models';
export class AnimalClassifications {
classes: Class[];
orders: Order[];
families: Family[];
genera: Genus[];
species: Species[];
} You then need a service to pull that data in from your API. This does NOT need to be an auto-entity service, it can just be a Plain Old Angular Service. ;) Something like: @Injectable({providedIn: 'root'})
export class AnimalClassificationsService {
// ... standard stuff ...
loadAll(): Observable<AnimalClassifications> {
return this.http.get<AnimalClassifications>('https://...');
}
} So now that you have your model and a service, you just need an action & effect that will handle the rest. This effect will use your custom action (standard ngrx action) to initiate the load, and when the data comes back...you split off the individual child entities. Using NgRx 8: export const loadAllAnimalClassifications = createAction('[AnimalClassifications] Load All');
export class AnimalClassificationsEffects {
// ... standard stuff ...
loadAllAnimalClassifications$ = createEffect(() =>
this.actions$.pipe(
ofType(loadAllAnimalClassifications),
exhaustMap(() =>
this.animalClassificationsService.loadAll().pipe(
map(classifications => {
// Now, dispatch LoadAllSuccess for each of the models contained within AnimalClassifications:
this.store.dispatch(new LoadAllSuccess(Class, classifications.classes);
this.store.dispatch(new LoadAllSuccess(Order, classifications.orders);
this.store.dispatch(new LoadAllSuccess(Family, classifications.families);
this.store.dispatch(new LoadAllSuccess(Genus, classifications.genera);
this.store.dispatch(new LoadAllSuccess(Species, classifications.species);
}),
catchError(err => {
// Handle error...we don't dispatch with this effect, so you do whatever you need to
})
)
, {dispatch: false});
} So here, you leverage the existing functionality of Auto-Entity entirely. You use its actions, and let it reduce your state, so you don't have to worry about writing a reducer yourself. You just need to create the initiating action for the whole process, then distribute the result. |
Awesome! That looks like a great way to handle it. I really appreciate you taking the time to respond, and come up with some starter code for me. It is great how Auto-entity is similar to a typical @ngrx/entity. For me it clarifies how it can be used in the context of vanilla ngrx. I am definitely going to give auto-entity a shot in my current project, rather than trying to build out all my own facades over a vanilla store. A bit off topic (I can definitely open a new issue if this warrants more discussion). Do you have any plans/interest in expanding the built-in selectors to include some functions that help with nested data? I've been working with some pretty complex nested data in ngrx these days, and have a whole bunch of utility selectors I've written to do things like output entities grouped by some parameter. For example, the Genus class might look like: export class Genus {
id: string;
name: string;
familyId: string;
} a handy selector I use regularly (and re-create often) is one that returns: export interface GenusByFamily {
[key: string]: Genus[];
} where the key is the parent Family ID. Another selector I re-create is similar, but it returns just a single Family's Genera. I'd be happy to submit PRs if more selectors are of interest. One other (likely more difficult) selector is one that transforms the raw store data, into a more appropriate "view model". Doing things such as converting datetime strings into actual date objects, or even building out full objects with related models attached. I'd love to hear any thoughts you might have on this, and again, am happy to submit some PRs as I work with auto-entity. Thanks again for your help! |
@mxdmedia Great to hear you'll be giving it a try! Let me know how it goes, and if you have any needs. Regarding selectors... Selectors is an area where you can create a countless numbers of them. It is a best practice to use selectors as much as possible to retrieve data from the store. That is a best practice we promote with Auto-Entity as well. The only difference is, we tend to wrap selector usage within facades. ;) There are probably some additional "common" selectors that could be added to Auto-Entity in the future. I would like to make sure that if we add more, that they are well and truly common enough to warrant being included. I did add selections (both single-entity and multi-entity), as well as edits (edit, change, endEdit) features, both the necessary actions and selectors, as those are pretty common use cases. Beyond these, I think we'll need to see how many people request the same selectors or features before we add more. One of my small concerns now is the size of the library. It is ~75k right now. It is a bit large, and at the moment we do not really have any way to break it down into smaller pieces that may be selectively added to a project. In the future I am thinking we may shift to an @ngrxae/* namespace, and break the library up a bit. But that is down the road a ways, needs more thought and planning. Once we do that, though, concerns about the size of the library (which, BTW, IS tree-shakable, so the final load to your deployment packages should generally be less than 75k unless you are very extensively using all the features) will fade once we have it broken out into multiple libs under a single namespace. Anyway, my general recommendation with selectors is, use them, use them extensively, they should proliferate, and wrap them inside of your facades (you can wrap them either in getter properties, or getter functions if you need parameterization). For parameterized data retrieval, since NgRx selectors don't really memoize parameterized selectors well, I usually just pipe off of another base selector, then handle the criteria in the pipe. You can also roll your own memoization as necessary (there are some good memo libs on github/npm that can help you there) to maintain those benefits when .pipe()ing off of other selectors. Hope all that helps!! |
I'll definitely provide you with feedback (and likely some questions) as I use auto-entity. Re: Selectors, I too am a huge proponent of 'selector all the things.' I find that selectors can get a bit tricky when trying to join data together. If you ever do namespace out features, having a 'join selectors' optional feature might be nice, but I fully see where you are coming from re: size. The way I do it now, is typically embedding a utility function (e.g.- a |
Let me know if you'd like me to open a new issue for things like this. I've started using auto-entity, and so far, so good. However, I haven't been able to find in the docs how to do one particular function that I can do with @ngrx/entity: specify a |
@mxdmedia Apologies for the late reply. Currently, I don't have anything in the lib for sorting. If you could open an issue specifying what you need, I'll see what I can do to get the functionality added for you. |
@mxdmedia Is it safe to assume your questions about how to handle custom use cases with reducers and effects have been answered? Can I close this issue? |
Absolutely! Thanks for your help on this. |
Since this was re-opened, I have one more similar, though slightly different use case I am trying to get working. Lets say I have the following models, handled nicely by // src/app/models/message.model.ts
export class Message {
id: string;
title: string;
body: string;
author: string; // ID of a user
}
// src/app/models/user.model.ts
export class User {
id: string;
firstName: string;
lastName: string;
username: string;
} My api has an endpoint: {
"messages": [
{"id": "abc", "title": "Message", "body": "Message body.", "author": "user-1"},
{"id": "def", "title": "Message2", "body": "Message2 body.", "author": "user-2"},
...
]
} However, one can also pass an {
"messages": [
...
],
"users": [
{"id": "user-1", ...},
{"id": "user-2", ...}
]
} Does auto-entity have enough flexibility to allow this internally, as both |
@mxdmedia Yeah, I re-opened it just so it would be visible to future visitors who might have similar questions. As for your question. I think this would be the same as before. Create a custom effect to split your result and dispatch the necessary success actions so Auto-Entity can handle the rest. You could handle the dynamic potential of the data easily enough in an effect: export const dynamicLoadMessages = createAction(
'[Messages] Load Many (Dynamic)',
props<{include: string[]}>()
);
export class MessageEffects {
// ... standard stuff ...
dynamicLoadMessages$ = createEffect(() =>
this.actions$.pipe(
ofType(dynamicLoadMessages),
map(action => action.include),
exhaustMap(include =>
this.messageService.loadManyDynamic(include).pipe(
map(dynamic => {
this.store.dispatch(new LoadManySuccess(Message, dynamic.messages);
if (dynamic.users) {
this.store.dispatch(new LoadManySuccess(Users, dynamic.users);
}
// If you can bring in more, handle it here; Possibly find a way to handle it entirely dynamically, even, if your include can bring in a wide range of possible alternative entities
}),
catchError(err => {
// Handle error...we don't dispatch with this effect, so you do whatever you need to
})
)
, {dispatch: false});
} Note the actions dispatched here: **Success. This is the key for any similar use case. You don't need to dispatch LoadMany, as that is an You can leverage this, along with custom effects, to handle just about any kind of potential data shape from any server, even if it does not explicitly conform to what Auto-Entity expects. This also allows you to centralize these complexities...these SIDE EFFECTS...into effects, allowing you to keep your data services clean and simple, and solely responsible for making HTTP calls and handling HTTP responses. Now, in the above example, I have created a new action and implied that a new service method would need to be created. FOR NOW, that is the case. However, with the upcoming @entity decorator for entity model classes, you will have a lot more power and control. Auto-Entity actions already support custom criteria...such as Model: @Entity({
excludeEffects: matching(EntityActionTypes.Load, EntityActionTypes.LoadAll, EntityActionTypes.LoadMany)
})
export class Message {
@Key id: string;
title: string;
body: string;
author: string;
} Service: const getIncludeParams = (criteria: { include: string[] }): { [param: string]: string | string[] } => {
const include = Array.join(criteria.include, ',');
const params = include.length ? { params: { include } } : undefined;
return params;
}
export class DynamicMessage {
message: Message;
user?: User;
}
export class DynamicMessages {
messages: Message[];
users?: User[];
}
export class MessageService implements IAutoEntityService<any> {
load(entityInfo: IEntityInfo, id: string, criteria: { include: string[] }): Observable<Message | DynamicMessage> {
const params = getIncludeParams(criteria);
const opts = params ? { params } : undefined;
return this.http.get<Message>(`${environment.baseUrl}/api/messages/${id}`, opts);
}
loadAll(entityInfo: IEntityInfo, criteria: { include: string[] }): Observable<Message[] | DynamicMessages> {
const params = getIncludeParams(criteria);
const opts = params ? { params } : undefined;
return this.http.get<Message>(`${environment.baseUrl}/api/messages`, opts);
}
loadMany(entityInfo: IEntityInfo, criteria: { include: string[] }): Observable<Message[] | DynamicMessages> {
const params = getIncludeParams(criteria);
const opts = params ? { params } : undefined;
return this.http.get<Message>(`${environment.baseUrl}/api/messages`, opts);
}
// ...other methods...
} Custom Effect: export class MessageEffects {
// ... standard stuff ...
load$ = createEffect(() =>
this.actions$.pipe(
ofEntityType(Message, EntityActionTypes.Load),
exhaustMap(action =>
this.messageService.load(action.info, action.keys, action.criteria).pipe(
map((dynamic: Message | DynamicMessage) => {
if (dynamic instanceof Message) {
this.store.dispatch(new LoadSuccess(Message, dynamic));
} else {
this.store.dispatch(new LoadSuccess(Message, dynamic.message));
if (dynamic.users) {
this.store.dispatch(new LoadSuccess(Users, dynamic.user));
}
// If you can bring in more, handle it here; Possibly find a way to handle it entirely dynamically, even, if your include can bring in a wide range of possible alternative entities
}
}),
catchError(err => {
// Handle error...we don't dispatch with this effect, so you do whatever you need to
})
)
, {dispatch: false});
// Implement loadAll$ and loadMany$ much the same as load$, just handle arrays of entities rather than single entities
} Auto-Entity will do certain things that follow a fairly common pattern. However, when particular use cases deviate from that pattern, that is where you can step back into raw NgRx and do anything you want. Auto-Entity explicitly provides that fallback option, as we cannot automate every possible scenario. We can still help you though, when you need to fall back and handle custom use cases...because you can always dispatch our Success or Failure actions to integrate your data with Auto-Entity-managed state in the end. As such, even when you do fall back onto raw NgRx as necessary, you should still not have to implement nearly as much as you might have had to in the past with just plain old NgRx (i.e. you do not need to create a full set of actions and effects for every initiation or result for every entity in your app...) |
Thanks again for more help. The action/effect at the top make sense. I will give that a try. On a related note..
This is how I was thinking it would be implemented, but couldn't figure out how. I look forward to seeing the new @entity decorator- it sounds like it is going to be quite powerful. |
I hope it adds the level of flexibility that will allow users like yourself, who are more on the "power user" end of the spectrum, to do the more complex things they need without having to expend a huge amount of effort creating actions and effects for everything. |
Closing to reduce issue count. |
I just came across this library, and it looks to meet most (perhaps all?) of my needs for a project i am working on. I've watched a couple of talks about it on youtube, and I am quite excited to give Auto-Entity a go!
One question I have, as it doesn't seem to be in the docs. How does one go about adding a custom reducer (reacting to actions from another entity's actions) for an auto-entity managed entity?
For example, my API includes a fairly deeply nested tree (5 levels deep). Say these 5 levels are from Animal Classifications (
Class
,Order
,Family
,Genus
Species
) where each has an id pointing to its parent. My API also has an endpoint for each with all the proper CRUD operations (/api/classes
,/api/orders
,/api/families
,/api/genera
,/api/species
). Additionally, to reduce the number of http requests, there is a read-only, GET/List endpoint:/api/animals
which returns a flattened version of the entire tree that looks like:Using vanilla ngrx, I'd create an Animals feature, that had just
loading
, anderror
as parameters, and the actionsAnimalActions.LoadClassifications
,AnimalActions.LoadClassificationsComplete
, andAnimalActions.LoadClassificationsError
. There would also be an Effect for Animals to fetch the data. Then in my other related features, I'd have my reducer do something like:Then do the same for the other entities. Is it possible to do something like this if
Genus
entities were managed using auto-entity? I see where one defines an essentially empty reducer function. But I presume the reducers are injected elsewhere. Any thoughts on the possibility, or how this might be accomplished would be appreciated! And thank you for writing this library, it looks like it will make ngrx use much easier to write and maintain!The text was updated successfully, but these errors were encountered: