0

I'm building an authentication app using the PEAN stack (i.e., PostgreSQL - ExpressJS - Angular - NodeJS).

I have two components: dashboard.component.ts and edit-profile.component.ts. They both subscribe to getUser in ngOnInit(), as follows:

ngOnInit(): void {
  console.log('ngOnInit triggered');
  this.authService.getUser().subscribe(this.getUserObserver);
}

Problem

The problem is that if I refresh the page (e.g. press the F5 key):

  • dashboard.component.ts does trigger ngOnInit(); while
  • edit-profile.component.ts does not trigger ngOnInit().

I've been able to confirm this in two ways:

  1. using console.log('ngOnInit triggered'); and
  2. using the Network tab in Developer Tools.

After the dashboard.component.ts page refresh:

  • Console:

    Screenshot dashboard 1

  • Network:

    Screenshot dashboard 2

After the edit-profile.component.ts page refresh:

  • Console:

    Screenshot edit profile 1

  • Network:

    Screenshot edit profile 2

Question

Why is ngOnInit() triggered if I refresh dashboard.component.ts but not triggered if I refresh edit-profile.component.ts?

Note: Those two components have different code (form groups, error messages, observers, etc.), but I don't think the code in the components is causing the problem. The biggest difference is the routing. See the relevant part of the app-routing.module.ts below. Also, auth guards should work as expected as long as a component makes an API call to the get-user API endpoint because both auth guards rely on the same information whether the get-user returns 200 or 400.

app-routing.module.ts

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'dashboard/:token', component: DashboardComponent, canActivate: [IfSignedIn] },
  {
    path: 'profile',
    component: MainProfileComponent,
    canActivate: [IfSignedIn],
    children: [
      { path: 'edit-profile', component: EditProfileComponent, canActivate: [IfSignedIn] },
    ],
  },
];

EDIT

See the full code for both components below.

dashboard.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from 'src/app/services/auth/auth.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements OnInit {
  showUserData: any = '';

  readonly getUserObserver = {
    next: (x: any) => {    
      this.showUserData = x;
    },

    error: (err: any) => {
      if (err.status == 401) {
        this.snackBar.open('Token invalid', 'Close', {
          duration: 5000,
          panelClass: ['red-snackbar'],
        });
      }

      if (err.status == 500) {
        this.snackBar.open('Internal server error', 'Close', {
          duration: 5000,
          panelClass: ['red-snackbar'],
        });
      }
    },
  };

  readonly editProfileConfirmationObserver = {
    next: (x: any) => {
      this.dashboardRouter.navigateByUrl('/refresh', { skipLocationChange: true }).then(() => {
        this.snackBar.open('Profile changes saved successfully', 'Close', {
          duration: 5000,
          panelClass: ['green-snackbar'],
        });
      });
    },

    error: (err: any) => {
      if (err.status == 401) {
        this.snackBar.open('Link invalid or expired, please edit your profile again', 'Close', {
          duration: 5000,
          panelClass: ['red-snackbar'],
        });
      }
    },
  };

  constructor(private authService: AuthService, private dashboardRouter: Router, private snackBar: MatSnackBar) {}

  ngOnInit(): void {
    console.log('ngOnInit triggered');
    this.authService.getUser().subscribe(this.getUserObserver);
    this.authService.editProfileConfirmationLink().subscribe(this.editProfileConfirmationObserver);
 }
}

edit-profile.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { Validators } from '@angular/forms';
import { AuthService } from 'src/app/services/auth/auth.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { EditProfileDialogComponent } from './dialogs/edit-profile-dialog/edit-profile-dialog.component';

@Component({
  selector: 'app-edit-profile',
  templateUrl: './edit-profile.component.html',
  styleUrls: ['./edit-profile.component.scss'],
})
export class EditProfileComponent implements OnInit {
  oldEmail: string = 'Initial value';
  showUserData: any = '';

  formEditProfile = new FormGroup({
    editProfileEmail: new FormControl('', [
      Validators.required,
      Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'),
    ]),
  });

  getErrorMessageEmail() {
    if (this.formEditProfile.controls['editProfileEmail'].hasError('required')) {
      return 'Email is required';
    }

    return this.formEditProfile.controls['editProfileEmail'].hasError('pattern') ? 'Not valid email' : '';
  }

  readonly getUserObserver = {
    next: (x: any) => {
      this.showUserData = x;

      this.oldEmail = x.email;
    },

    error: (err: any) => {
      this.snackBar.open('User data fetched unsuccessfully', 'Close', {
        duration: 5000,
        panelClass: ['red-snackbar'],
      });
    },
  };

  onSubmitEditProfile() {
    const editProfileObserver = {
      next: (x: any) => {
        this.editProfileDialog.open(EditProfileDialogComponent, {
          panelClass: 'custom-dialog-container',
          width: '550px',
        });
      },

      error: (err: any) => {
        if (err.status == 409) {
          this.snackBar.open(err.error.message, 'Close', {
            duration: 5000,
            panelClass: ['red-snackbar'],
          });
        }

        if (err.status == 401) {
          this.snackBar.open('Token invalid or expired, please try again', 'Close', {
            duration: 5000,
            panelClass: ['red-snackbar'],
          });
        }

        if (err.status == 400) {
          this.snackBar.open('Something went wrong', 'Close', {
            duration: 5000,
            panelClass: ['red-snackbar'],
          });
        }

        if (err.status == 500) {
          this.snackBar.open('Internal server error', 'Close', {
            duration: 5000,
            panelClass: ['red-snackbar'],
          });
        }
      },
    };
    this.authService.editProfileNewEmail(this.formEditProfile.value.editProfileEmail!).subscribe(editProfileObserver);
  }

  constructor(private authService: AuthService, private editProfileRouter: Router, private snackBar: MatSnackBar, private editProfileDialog: MatDialog) {}

  ngOnInit(): void {
    console.log('ngOnInit triggered');
    this.authService.getUser().subscribe(this.getUserObserver);
  }
}
Rok Benko
  • 14,265
  • 2
  • 24
  • 49
  • Not enough information here. But it’s extremely unlikely ngOninit doesn’t trigger - that would break thousands of apps.. i’d look for typos or other issues. – MikeOne Aug 09 '23 at 17:10
  • I think the reason why your `edit-profile.component.ts` doesn't trigger anything in its own `OnInit` lifecycle hook is that the component is not rendered initially... since it is an 'edit', probably is it waiting for a click on the dashboard, then this component will show up calling the `ngOnInit`. Is this how it works? – Andres2142 Aug 09 '23 at 17:18
  • The reason could also be not recognizing component as valid, like mistake by using @Injectable() decorator – Adam Zabrocki Aug 09 '23 at 17:45
  • @MikeOne What information do I need to add? I know, I'm very confused. On button click, everything works, but on page refresh or if I type in the search bar `http://localhost:4200/profile/edit-profile`, the page breaks (i.e., `ngOnInit()` not triggered and consequently no API call is made). See [this GIF](https://i.stack.imgur.com/jkOLH.gif), which I made before I put `console.log('ngOnInit triggered');` in my code. That's why, on button click, there's no *ngOnInit triggered* in the console. What typos or other issues do you refer to? Wouldn't I get an error in the case of typos and stuff? – Rok Benko Aug 10 '23 at 08:01
  • @Andres2142 The Edit profile component shows up when I click on the *Edit profile* button in the header dropdown. See [this GIF](https://i.stack.imgur.com/jkOLH.gif). – Rok Benko Aug 10 '23 at 08:05
  • @AdamZabrocki I don't use `@Injectable()` in any of these two components. Anyway, I'm curious: why would using `@Injectable()` cause such a problem? – Rok Benko Aug 10 '23 at 08:09
  • @MikeOne I edited my question and added the full code for both components. – Rok Benko Aug 10 '23 at 08:37
  • @Andres2142 I edited my question and added the full code for both components. – Rok Benko Aug 10 '23 at 08:37
  • @AdamZabrocki I edited my question and added the full code for both components. – Rok Benko Aug 10 '23 at 08:37
  • @RokBenko I had an idea, that Your component is actually not a component, but a service which was obviously not the case. The example of the similar issue would be https://stackoverflow.com/questions/35110690/ngoninit-not-being-called-when-injectable-class-is-instantiated – Adam Zabrocki Aug 11 '23 at 09:58

1 Answers1

1

I think it is because you have a guard (i.e., canActivate) on your profile route. If that guard returns false, it will not actually load your route and child components.

As @hawks mentioned in the comment below:

You need to understand when a guard is executed. Guards are executed before the component is loaded/rendered. So if get-user data is not available in the guard it will return false thus your route not being loaded. Therefore no ngOnInit() is executed because the component has not been rendered. You should fetch authentication state before the component is loaded so the guards can work as expected.

Screenshot

Rok Benko
  • 14,265
  • 2
  • 24
  • 49
Paul
  • 460
  • 2
  • 7
  • Thanks for the response. I don't think this is a problem. Actually, the auth guard allows the route to be accessible, but only the header and footer are rendered (see [this GIF](https://i.stack.imgur.com/jkOLH.gif)). This is because the auth guard needs information from the `get-user` API call, which is not made because `ngOnInit()` is *not* triggered on page refresh. Consequently, the auth guard doesn't get `200` or `400`, so it doesn't know whether it should allow access to the route or not. Also, see my edited question and discussion in the comments with other SO members above. – Rok Benko Aug 10 '23 at 09:00
  • @Paul is right, you need to understand when a guard is executed. Guards are executed before the component is loaded/rendered. So if get-user data is not available in the guard it will return false thus your route not being loaded. Therefore no ngOnInit is executed because the component has not been rendered. You should fetch authentication state before the component is loaded so the guards can work as expected. https://images.indepth.dev/images/2019/08/image-216.png – hawks Aug 10 '23 at 09:58
  • @RokBenko tagged wrong person – hawks Aug 10 '23 at 16:13
  • @hawks I removed the guard from the Edit profile component, and `ngOnInit()` is triggered. But, of course, I want to have auth guards, so this is not a solution. How can I fetch auth state if not in `ngOnInit()`? How do I *fetch authentication state before the component is loaded*? How should I implement my auth guards? – Rok Benko Aug 10 '23 at 16:52
  • 1
    The component is not an appropriate place for fetching auth state. You would probably want to check the authentication status by calling a service and by utilizing RxJS methods return the value. It's a broad topic, with multiple different strategies - it needs a separate topic, since it also would touch interceptor issue, presented on @hawks diagram – Adam Zabrocki Aug 11 '23 at 09:50
  • @RokBenko as Adam has replied you should fetch the state in one place and save it. To persist between reloads usually its saved it localStorage or sessionStorage or cookies. When you load the app fetch the state and save it in sessionStorage then in the guard read from sessionStorage. – hawks Aug 11 '23 at 09:58
  • Guys, thanks for your help! I managed to solve the problem. Now I use `localStorage`, and auth guards work as expected. Consequently, the components get rendered, and `ngOnInit()` works fine. This sentence was crucial to my understanding of the problem: *You should fetch authentication state before the component is loaded so the guards can work as expected*. Before, my auth guards were not working because the sign-in status was fetched inside components, but components didn't get rendered because auth guards were waiting for the sign-in state. – Rok Benko Aug 12 '23 at 13:37
  • 1
    @Paul I edited your answer to be more clear and to add input from other StackOverflow members. Please leave it as it is now, because the combined information from all of you helped me understand the problem. – Rok Benko Aug 12 '23 at 13:40