Reputation: 117
I'm trying to make a sequiental requests to ngrx-store to save two different entities in two random mongodb collections and need to have answer from first ngrx-store effect to use data in another one on create entity referring to first one.
What I have:
reducer
export interface UserState {
currentUser: IUser | null;
checkUserResult: any | null;
user: IUser | null;
users: IUser[] | null;
errorMessage: string | null;
isLoading: boolean;
isLoaded: boolean;
isError: boolean;
}
export function userReducer(
state = initialUserState,
action: fromUser.UserActions
): UserState {
switch (action.type) {
case fromUser.USER_CREATE: {
return {
...state,
isLoading: true,
};
}
case fromUser.USER_CREATE_SUCCESS: {
return {
...state,
user: {
_id: action.user._id,
userLogin: action.user.userLogin,
userPassword: action.user.userPassword,
userEmail: action.user.userEmail,
userPhone: action.user.userPhone,
userRole: action.user.userRole,
userType: action.user.userType,
userRef: action.user.userRef,
},
checkUserResult: null,
errorMessage: null,
isLoading: false,
isLoaded: true,
isError: false,
};
}
case fromUser.USER_CREATE_FAILURE: {
return {
...state,
errorMessage: "Can not create user",
isError: true,
};
}
[...]
}
action
export class UserCreate implements Action {
readonly type = USER_CREATE;
constructor(public payload: IUser) {}
}
export class UserCreateSuccess implements Action {
readonly type = USER_CREATE_SUCCESS;
constructor(public user: IUser) {}
}
export class UserCreateFailure implements Action {
readonly type = USER_CREATE_FAILURE;
constructor(public error: string) {}
}
effect
@Effect()
createUser$: Observable<Action> = this.action$.pipe(
ofType(UserActions.USER_CREATE),
map((action: UserActions.UserCreate) => action.payload),
exhaustMap((payload) => {
return this.userDataService.createUser(payload).pipe(
map((data) => {
return new UserActions.UserCreateSuccess(data);
}),
catchError((err) => of(new UserActions.UserCreateFailure(err)))
);
})
);
selector
export const getUserState = createSelector(
fromFeature.getAppState,
(state: fromFeature.AppState) => state.users
);
and service
createUser(user: IUser) {
this.store.dispatch(new fromStore.UserCreate(user));
return this.store.select(fromStore.getUserState);
}
as well as another service (user-data.service.ts) which methods calls from effect
createUser(user: IUser): Observable<IUser> {
const url = `${this.BASE_URL}/user/create`;
return this.http.post<IUser>(url, { ...user });
}
all this setup works while I create only one entity from component
user-create.component.ts
onCreateUser() {
this.us
.createUser({
userLogin: this.createUserForm.value.userLogin,
userPhone: this.createUserForm.value.userPhone,
userEmail: this.createUserForm.value.userEmail,
userPassword: this.createUserForm.value.userPassword,
userRole: this.createUserForm.value.userRole,
userRef: this.createUserForm.value.userRef,
userType: this.createUserForm.value.userType,
})
.subscribe((user) => {
if (user) {
this.createUserForm.reset();
this.router.navigate(["/users"]);
this.us.getUsersList();
}
});
}
Other logic to create different entities are equal to listed above. And when I try to call action inside action within component like this
this.us
.createUser({
userLogin: this.createContactUserForm.value.userPhone,
userPhone: this.createContactUserForm.value.userPhone,
userEmail: this.createContactUserForm.value.userEmail,
userPassword: this.createContactUserForm.value.userPassword,
userRole: this.createContactUserForm.value.userRole,
userRef: this.createContactUserForm.value.userRef,
userType: this.createContactUserForm.value.userType,
})
.subscribe((userState) => {
if (userState.user) {
this.cs
.createContact({
contactFirstName: this.createContactUserForm.value
.contactFirstName,
contactLastName: this.createContactUserForm.value.contactLastName,
contactPatronymicName: this.createContactUserForm.value
.contactPatronymicName,
contactPhone: this.createContactUserForm.value.userPhone,
contactUserId: userState.user._id,
})
.subscribe((contactState) => {
if (contactState) {
this.router.navigate(["/contacts"]);
}
});
}
});
it works as intended, but later, when I try to make any other action - it brokes application state. For example I can't edit or delete contacts after it. On other hand if I split logic to two independent calls - it won't work, because at moment when createContact method calls - application didn't know yet createdUser _id or userState.user._id and throw an error.
I think, here must be a more complex logic, but how make it correct - I have no clue. Please, suggest me proper way to perform this.
Thanks in advance.
Upvotes: 0
Views: 225
Reputation: 3588
You have issues related to your subscription management.
The things that I will recommend to you are more like a general idea of how you should work with NGRX
First, let's look at your "dispatcher" service / model or however people are calling the service that takes care of dispatching actions nowadays.
createUser(user: IUser) {
this.store.dispatch(new fromStore.UserCreate(user));
return this.store.select(fromStore.getUserState);
}
Having a separate method to call the store
is good practice, returning the store selector from there, not so much. There are two reasons for that:
Refactored version
user$ = this.store.select(fromStore.getUserState);
createUser(user: IUser): void {
this.store.dispatch(new fromStore.UserCreate(user));
}
Now let's take a look at the component where you are initiating the create user request
user-create.component.ts
onCreateUser() {
this.us
.createUser({
userLogin: this.createUserForm.value.userLogin,
userPhone: this.createUserForm.value.userPhone,
userEmail: this.createUserForm.value.userEmail,
userPassword: this.createUserForm.value.userPassword,
userRole: this.createUserForm.value.userRole,
userRef: this.createUserForm.value.userRef,
userType: this.createUserForm.value.userType,
})
.subscribe((user) => {
if (user) {
this.createUserForm.reset();
this.router.navigate(["/users"]);
this.us.getUsersList();
}
});
}
When watching this block of code in the context of your implementation, we can see that each time when we create a new user we are creating a new subscription
, which means that after we create our first user and we attempt to create a second one, we will be creating a new subscription
executing the code inside the subscribe method + we will be also executing the code in the old subscription
, because we never unsubscribe.
Also if we already had created a user we will get inside the if(user)
block before creating successfully our new user, because we already have a user entity inside our store.
Refactored
user.service.ts
user$ = this.store.select(fromStore.getUserState);
userCreatedSuccesfuly$ = this.actions$.pipe(ofType(UserActions.USER_CREATE))
createUser(user: IUser): void {
this.store.dispatch(new fromStore.UserCreate(user));
}
user-create.component.ts
onCreateUser() {
this.us
.createUser({
userLogin: this.createUserForm.value.userLogin,
userPhone: this.createUserForm.value.userPhone,
userEmail: this.createUserForm.value.userEmail,
userPassword: this.createUserForm.value.userPassword,
userRole: this.createUserForm.value.userRole,
userRef: this.createUserForm.value.userRef,
userType: this.createUserForm.value.userType,
})
this.us.userCreatedSuccesfuly$.pipe(take(1))
.subscribe(() => {
this.createUserForm.reset();
this.router.navigate(["/users"]);
this.us.getUsersList();
});
}
So the difference here is that now instead of depending on changes inside the user field (from the store) we are depending on the success action, that will be fired after a new user had been created, also by using the take(1)
operator we are making sure that there will be no memory leaks because the subscription will be killed once that the user has been created.
(side-note: there is still the possibility of a memory leak if the user creation has failed, but I don't want to go in more complex solutions)
And now for the nested subscriptions
So overall subscribe
inside of subscribe
is a big no-no
this.us
.createUser({
userLogin: this.createContactUserForm.value.userPhone,
userPhone: this.createContactUserForm.value.userPhone,
userEmail: this.createContactUserForm.value.userEmail,
userPassword: this.createContactUserForm.value.userPassword,
userRole: this.createContactUserForm.value.userRole,
userRef: this.createContactUserForm.value.userRef,
userType: this.createContactUserForm.value.userType,
})
.subscribe((userState) => {
if (userState.user) {
this.cs
.createContact({
contactFirstName: this.createContactUserForm.value
.contactFirstName,
contactLastName: this.createContactUserForm.value.contactLastName,
contactPatronymicName: this.createContactUserForm.value
.contactPatronymicName,
contactPhone: this.createContactUserForm.value.userPhone,
contactUserId: userState.user._id,
})
.subscribe((contactState) => {
if (contactState) {
this.router.navigate(["/contacts"]);
}
});
}
});
Refactored
user.service.ts
user$ = this.store.select(fromStore.getUserState);
userCreatedSuccesfuly$ = this.actions$.pipe(ofType(UserActions.USER_CREATE_SUCCESS))
createUser(user: IUser): void {
this.store.dispatch(new fromStore.UserCreate(user));
}
contact.service.ts
contact$ = this.store.select(fromStore.getUserState);
contactCreatedSuccesfuly$ = this.actions$.pipe(ofType(ContactActions.CONTACT_CREATE_SUCCESS))
createUser(contact: IContact): void {
this.store.dispatch(new fromStore.ContactCreate(contact));
}
some-cmp.component.ts
doSomething(){
this.us.createUser({
userLogin: this.createContactUserForm.value.userPhone,
userPhone: this.createContactUserForm.value.userPhone,
userEmail: this.createContactUserForm.value.userEmail,
userPassword: this.createContactUserForm.value.userPassword,
userRole: this.createContactUserForm.value.userRole,
userRef: this.createContactUserForm.value.userRef,
userType: this.createContactUserForm.value.userType,
})
this.us.userCreatedSuccesfuly$.pipe(
switchMap(() => this.us.user$),
filter(x => !!x),
tap(userState => {
this.cs
.createContact({
contactFirstName: this.createContactUserForm.value
.contactFirstName,
contactLastName: this.createContactUserForm.value.contactLastName,
contactPatronymicName: this.createContactUserForm.value
.contactPatronymicName,
contactPhone: this.createContactUserForm.value.userPhone,
contactUserId: userState.user._id,
})
}),
switchMap(() => this.cs.contactCreatedSuccesfuly$),
take(1)
)
.subscribe((userState) => {
this.router.navigate(["/contacts"]);
});
}
So let us go through the last block of code:
switchMap
We refer to userCreatedSuccesfuly$
and we are saying, whenever this observable emits, switch to a new observable that is this.us.users$
.filter
Filter the passing values in such a way that only truthy values can pass trough the stream (e.g. only if there is a user saved inside in our store)tap
Once a value reaches this point create a contact, by using the incoming value, without affecting the value of the current stream (e.g. create a contact with the user information)switchMap
Here we are referring to the this.us.users$
and we are saying whenever a value gets to this point, switch to a new observable that is this.cs.contactCreatedSuccesfuly
take
Here we make sure that we will clear our subscription after we have succeeded with the creation of new contactsubscribe
by calling this method we are triggering the streamOh boy, that was long, anyway, my advice to you is to spend some more time on learning rxjs
in general
Upvotes: 2