Search code examples
angularangular-routingngrx

NGRX flow and routing - Component being acessed too many times


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:

enter image description here

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.

enter image description here

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}

Solution

  • 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.

    Example

    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}'`;
    })