Angular Code Snippets - aakash14goplani/FullStack GitHub Wiki


Subject vs BehaviorSubject

  • We have a user object with four properties:

    user = {
       public email: string,
       public id: string,
       private token: string,
       private tokenExpirationDate: Date
    }
  • If we want to take note of changes in user object as a whole i.e. new user added or existing user deleted, we could set up a subscription to the user

  • If you don't care about every change to the user, and you're only interested with one property of user and don't want to have an ongoing subscription, just want to capture if that property changed, we could set up a BehaviorSubject

  • The difference is that BehaviorSubject also gives subscribers immediate access to the previously emitted value even if they haven't subscribed at the point of time that value was emitted. explanation with example

  • Example we can get access to be currently active user even if we only subscribe after that user has been emitted. We can fetch data at this point of time, even if the user logged in before that point of time which will have been the case, we get access to that latest user.

    user = new Subject<User>();
    user = new BehaviorSubject<User>(null);
    ...
    this.user.next(user);
  • Now the thing just is I don't want to set up an ongoing subscription and still I need to subscribe to get that user, so to make sure I only get the user once and thereafter I'm done, of course we could manually immediately call unsubscribe() thereafter or you use pipe and then a special operator RxJS gives you and that would be the take() operator.

  • take() is called as a function and you simply pass a number to it and I pass 1 here and what this tells RxJS is that I only want to take one value from that observable and thereafter, it should automatically unsubscribe. So this manages the subscription for me.

    return this.authService.user.pipe(
       take(1),
       ....
    );
  • We can use HTTP request observable and set it up in there and attach our token which we get out of the user. The problem just is here, we're in subscribe of an observable and here, we create yet another observable. In the end, I want to return this observable here but returning from inside subscribe doesn't work, we have to return on the top level of this method.

  • The solution is that we pipe these two observables, the user observable and the HTTP observable together into one big observable. You simply add another operator here to pipe for the user observable and there, we can use exhaustMap().

  • exhaustMap() waits for the first observable, for the user observable to complete which will happen after we took the latest user. Thereafter, it gives us that user, so in exhaustMap() we pass in a function, there we get the data from that previous observable and now we return a new observable in there which will then replace our previous observable in that entire observable chain.

    return this.authService.user.pipe(
       take(1),
       exhaustMap(user => {
         return this.http.get<Recipe[]>(
           'https://angular-practice-166c4.firebaseio.com/posts.json',
           { params: new HttpParams().set('auth', user.token) }
         );
       }),
       map(finalObservable => { 
          ...
       })
    );
  • So we start of with a user observable and once we're done with that, this will be replaced in that observable chain with the inner observable we return inside of that function we pass to exhaustMap(). So in there, I will return this HTTP request where I get my recipes. This is now returned inside of exhaust map and therefore this entire observable chain now switches to this HTTP observable.


URLTree

  • What we should make sure though is that user is not able to access feature pages or particular URL e.g. /recipes, if they are not authenticated. In the past with older Angular versions, you had to manually redirect, if not, navigate him away

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
       return this.authService.user.pipe(
          tap(isAuth => {
             if (!isAuth) {
                this.router.navigate(['/auth']);
             }
          })
       );
    }
  • Nothing wrong with this but in some edge cases, this could lead to race conditions with multiple redirects that kind of interfere with each other and therefore, instead of using this approach, you can now also use a different one and not return true or false here but return URL tree.

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
       return this.authService.user.pipe(
          map(userData => {
             const isAuth = !!userData;
             if (isAuth) {
                return true;
             }
             return this.router.createUrlTree(['/auth']);
          })
       );
    }
  • The one issue we have here is that we essentially set up an ongoing subscription here though. This user subject of course can emit data more than once and I don't want that here, this can lead to strange side effects if our guard keeps on listening to that subject, instead we want to look inside the user value for one time only and then not care about it anymore unless we run the guard again and therefore here just as in other places too, we should use take one to make sure that we always just take the latest user value and then unsubscribe for this guard execution so that we don't have an ongoing listener

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
       return this.authService.user.pipe(
          take(1),
          map(userData => {
             const isAuth = !!userData;
             if (isAuth) {
                return true;
             }
             return this.router.createUrlTree(['/auth']);
          })
       );
    }

forChild() vs forRoot()

  • We can also move the feature components related route configuration away from the app (main) routing module. For that, in the router module for that feature component, you could add forChild().

  • Now forRoot() is only used once essentially, here in the app routing module where we use forRoot() to configure our root routes. In a feature module which you then plan on importing into your app module, you would use forChild() and this will automatically merge the child routing configuration with the root routes.

  • Feature Component

    const appRoute: Routes = [
       { path: '', component: ShoppingListComponent },
       { path: '**', redirectTo: '/page-not-found' } */
       { path: '**', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} }
    ];
    @NgModule({
       imports: [ RouterModule.forChild(appRoute) ],
       exports: [ RouterModule ]
    })
    export class ShoppingListRouting {}
  • Feature Module

    @NgModule({
       declarations: [
          ShoppingListComponent,
          ShoppingEditComponent
       ],
       imports: [
          SharedModule,
          FormsModule,
          ShoppingListRouting
       ]
    })
    export class ShoppingListModule {}
  • App Routing

    const recipesAppRoute: Routes = [
       { path: '', redirectTo: '/recipes', pathMatch: 'full' },
       // { path: 'recipes', loadChildren: './recipes/recipes.module#RecipesModule' }
       { path: 'auth', loadChildren: () => import('./auth/auth.module').then((mod) => mod.AuthModule) },
       { path: 'recipes', loadChildren: () => import('./recipes/recipes.module').then((mod) => mod.RecipesModule) },
       { path: 'shopping-list', loadChildren: () => import('./shopping-list/shopping-list.module').then((mod) => mod.ShoppingListModule) },
       { path: '**', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} }
    ];
    @NgModule({
       imports: [
          RouterModule.forRoot(recipesAppRoute, { preloadingStrategy: PreloadAllModules })
       ],
       exports: [
          RouterModule
       ]
    })
    export class AppRoutingModule {}
  • App Module

    imports: [
      BrowserModule,
      FormsModule,
      ReactiveFormsModule,
      HttpClientModule,
      AppRoutingModule,
      SharedModule
    ],

Single Import vs Multiple Import

Single Import: import { Ingredients } from '../../shared/ingredients.model';
Multiple Import: import * as ShoppingListActions from './shopping-list.actions';

⚠️ **GitHub.com Fallback** ⚠️