After I've implemented NGRX stores in my APP, I found out that my HomeComponent is being loaded too many times.
The flow is like below, from beggining:
1 - When the page is called, trys to load the dashboard, but the AuthGuard tells me that the user is not logged in and it loads the LoginComponent.
app-routing.module.ts
const routes: Routes = [
{
path: 'login',
loadChildren: './landing/landing.module#LandingModule'
},
{
path: '',
canActivate: [AuthGuard],
loadChildren: './dashboard/dashboard.module#DashboardModule'
}
];
2 - Then, user chooses to login via Facebook.
login.component.ts
signInWithFacebook() {
this.store.dispatch(new FacebookLogIn());
}
3 - The reducer is called, call my LoginService and if the authentication is fine, send to LogInSuccess effect. To resume, I won't post this part.
4 - If login succeded, I have to load other info about the user, so I call other stores and just then, navigate to my DashboardComponent.
@Effect({ dispatch: false })
LogInSuccess: Observable<any> = this.actions.pipe(
ofType(LoginActionTypes.LOGIN_SUCCESS),
tap(action => {
this.zone.run(() => {
this.store.dispatch(new GetData(action.payload.user.email));
this.store.dispatch(new GetData2(action.payload.user.uid));
localStorage.setItem('user', JSON.stringify(action.payload.user));
this.router.navigate(['/dashboard'])
});
})
);
5 - Dashboard loads HomeComponent together.
dashboard-routing.module.ts
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard],
children: [
{
path: '',
component: HomeComponent,
},
...
...
]
}
6 - The store calls result in this:
7 - And here is the problem. If I do a console.log in HomeComponent, I see that it have being called 1 time for each store called, as bellow.
Questions are:
Why?
What should I do to prevent all those unecessary loads?
If I remove one of the dispatches above, it goes only 3 times to the HomeComponent, and not 5 as the picture because it removes 2 of the effects.
-- Update --
HomeComponent.ts
isTermSigned = false;
homeInfo = {
isBeta: false,
isTermSigned: false,
displayName: '',
email: ''
};
homeSubscription: Subscription;
constructor(
private afs: AngularFirestore,
private router: Router,
private store: Store<AppState>
) { }
ngOnInit() {
this.homeSubscription = combineLatest(
this.store.pipe(select(selectData)),
this.store.pipe(select(selectStatusLogin))
).subscribe(([data, login]) => {
console.log(login);
if (login.user) {
this.homeInfo = {
isBeta: data.isBeta,
isTermSigned: data.isBeta,
displayName: login.user.displayName,
email: login.user.email
};
}
});
}
-- Update 2 -- Here is the important part of the data store
data.action.ts
export class GetData implements Action {
readonly type = PlayerActionTypes.GET_BETA_USER;
constructor(public payload: any) {}
}
export class GetDataSuccess implements Action {
readonly type = PlayerActionTypes.GET_DATA_SUCCESS;
constructor(public payload: any) {}
}
data.effect.ts
@Effect()
GetData: Observable<any> = this.actions.pipe(
ofType(PlayerActionTypes.GET_DATA),
mergeMap(email =>
this.dataService
.getData(email)
.then(data=> {
return new GetDataSuccess({
isBeta: data.email ? true : false,
isTermSigned: data.acceptTerms ? true : false
});
})
.catch(error => {
return new GetDataError({
isBetaUser: false,
isTermSigned: false
});
})
)
);
@Effect({ dispatch: false })
GetDataSuccess: Observable<any> = this.actions.pipe(
ofType(PlayerActionTypes.GET_DATA_SUCCESS),
tap(action => {
localStorage.setItem('data', JSON.stringify(action.payload));
})
);
data.reducer.ts
export interface State {
isBeta: boolean;
isTermSigned: boolean;
}
export const initialState: State = {
isBeta: false,
isTermSigned: false
};
export function reducer(state = initialState, action: All): State {
switch (action.type) {
case DataActionTypes.GET_DATA_SUCCESS: {
return {
...state,
isBeta: action.payload.isBeta,
isTermSigned: action.payload.isTermSigned
};
}
case DataActionTypes.GET_DATA_ERROR: {
return {
...state,
isBeta: action.payload.isBeta,
isTermSigned: action.payload.isTermSigned
};
}
...
default: {
const data = JSON.parse(localStorage.getItem('data'));
if (data) {
return {
...state,
isBeta: betaUser.isBeta,
isTermSigned: betaUser.isTermSigned
};
} else {
return state;
}
}
}
}
data.selector.ts
import { AppState } from '../reducers';
export const selectData = (state: AppState) => state.data;
-- Update 3 --
Another thing that might help and is breaking my mind, when I logout, one, and only one, effect is called but my HomeComponent, which has no redirect to it at all, is called twice:
{isAuthenticated: true, user: {…}, errorMessage: null}
{isAuthenticated: false, user: null, errorMessage: null}
I'm not sure to fully understand your context and your needs, but I think your HomeComponent
is not loaded multiple times. However, observable created with combineLatest
receives multiple times the same value.
May I suggest you 2 possible improvments :
1) Use selectors to compose multiple slice of the store.
For example, you can create a getHomeInfo
selector to receive all the informations you need, and avoid to call combineLatest
inside HomeComponent
. It's cleaner, better documented, and also better for next point.
2) Use memoized selectors with createSelector
Check this good post from Todd Motto.
Memoized selectors will avoid useless computation and also useless value emitted in observables. You just get notified in case of value updated only.
To illustrate these 2 points, I created a project on stackblitz: https://stackblitz.com/edit/angular-ajhyz4
Without createSelector
:
export const getValueWithoutCreateSelector = (state) => {
return state.app.value;
};
With createSelector
:
export const getValue = createSelector(getAppState, (state) => {
return state.value;
});
A composed selector :
export const getCustomMessage = createSelector(getValue, getText,
(value, text) => {
return `Your value is ${value} and text is '${text}'`;
})