Angular Single Page Application – TODOs

By | October 8, 2020

This post is going to walkthrough things to be considered when making design decisions on an Angular SPA application.

Application Structure

The application should be structured according to the recommendations from the official Angular documentation (https://angular.io/docs). We also want to have additional recommendations from NGRX documentation (https://ngrx.io/docs) for application structure.

It is recommended to divide projects in the following modules: –

  • Core Module
    • It is an independent module, it loads as the first module in the application and provides a reusable shell. It provides the structure that encapsulates the header, footer and the main body of the application.
    • components folder : It contains presentational components only
    • containers folder: It contains container components only
    • state folder: It contains module state classes
    • model folder: It contains global model objects that are used in core too.
  • Shared Module
    • The intent of the shared module is to keep things shared among modules but are not needed globally.
    • components folder : It contains presentational components only
    • model folder: It contains global model objects
  • Feature Modules
    • Feature Module A
      • This module contains things needed for Feature A.
      • components folder : It contains presentational components only
      • containers folder: It contains container components only
      • state folder: It contains module state classes
    • Feature Module B
      • This module contains things needed for Feature B.
      • components folder : It contains presentational components only
      • containers folder: It contains container components only
      • state folder: It contains module state classes
    • … more features
|-- app
    |-- modules -> Defines a feature, example: Feature A, Feature B, Feature C
        |-- feature-a
            |-- components (1:n components)
                |-- one or more presentational.component.ts|html|scss|spec
            |-- containers (1:n components)
                |-- feature-a-shell.component.ts|html|scss|spec 
            |-- state
                |-- index.ts
                |-- feature-a.actions.ts
                |-- feature-a.effects.ts
                |-- feature-a.reducer.ts                   
            |-- feature-a-routing.module.ts
            |-- feature-a.module.ts 
        |-- feature-b
             |-- components (1:n components)
                 |-- one or more presentational.component.ts|html|scss|spec
             |-- containers (1:n components)
                 |-- feature-b-shell.component.ts|html|scss|spec 
             |-- state
                 |-- index.ts
                 |-- feature-b.actions.ts
                 |-- feature-b.effects.ts
                 |-- feature-b.reducer.ts                   
             |-- feature-b-routing.module.ts
             |-- feature-b.module.ts             
    |-- core --> ***It is not dependent on any module, provides a reusable shell***
        |-- components (1:n components)
            |-- header -> app header
                |-- header.component.ts|html|scss|spec.ts
            |-- footer -> app footer
                |-- footer.component.ts|html|scss|spec.ts        
        |-- containers (1:n components)
            |--shell
                |-- application-shell.component.ts|html|scss|spec  
        |-- state
            |-- app.state.ts
        |-- [+] security -> simply handles the authentication-cycle of the user         
        |-- guards -> contains all of the guards I use to protect different routes in my applications
            |-- auth.guard.ts -> Sample files here
            |-- no-auth-guard.ts
            |-- admin-guard.ts
            |-- module-import-guard.ts -> Throws exception if already loaded module. We should load module once.        
        |-- http -> handles stuff like http calls from our application
            |-- api.service.ts|spec.ts  -> global http request service that wraps http client
        |-- interceptors -> This allows us to catch and modify the requests and responses from our API calls
            |-- api-prefix.interceptor.ts
            |-- error-handler.interceptor.ts
            |-- http.token.interceptor.ts
        |-- services -> All additional singleton services are placed in the services folder
            |-- analytics.service.ts
            |-- logger.service.ts        
        |-- core.module.ts
    |-- shared --> ***Not dependent on any feature module, provides reusable components across application***
        |-- components
            |-- loader
                |-- loader.component.ts|html|scss|spec.ts
            |-- buttons
                |-- favorite-button
                    |-- favorite-button.component.ts|html|scss|spec.ts
                |-- collapse-button
                    |-- collapse-button.component.ts|html|scss|spec.ts        
        |-- directives
            |-- auth.directive.ts|spec.ts
        |-- pipes
            |-- capitalize.pipe.ts
            |-- safe.pipe.ts
        |-- models -> models used across the application
            |-- user.model.ts
            |-- server-response.ts
    |-- configs
        |-- environment.ts
        |-- environment.prod.ts   
        |-- environment.stage.ts  
|-- assets
    |-- images
        |-- svg images/images sized as per resolution
    |-- fonts  
    |-- scripts -> Javascript files local copy rather than CDN 
    |-- styles
        |-- modules   
            |-- _colors.scss
            |-- _mixins.scss
        |-- partials
            |-- _base.scss
            |-- _forms.scss
            |-- _layout.scss
            |-- _typography.scss
            |-- _responsive.partial.scss
        |-- vendor
            |-- vendor styling/overrides
        |-- themes
            |-- default.scss        
        |-- styles.scss

Routing

Importing the Angular Router

  • Router module for Root (App global)
    • RouterModule.forRoot([])
    • Declares the router directives
    • Manages our route configuration
    • Registers the router service
    • Used once for the application
  • Router module for Child (Child routes for feature)
    • RouterModule.forchild([])
    • Declares the router directives
    • Manages our route configuration
    • Does NOT register the router service
    • Used in feature modules
  • Sample Routes
[
    { path: 'products', component: ProductListComponent },
    { path: 'products/:id', component: ProductDetailComponent },
    { path: 'products/:id/edit', component: ProductEditComponent },
    { path: 'orders/:id/items/:itemId', component: OrderItemDetailComponent }, //Placeholders in route 
    { path: 'welcome', component: WelcomeComponent },
    { path: '', redirectTo: 'welcome', pathMatch: 'full'},
    { path: '**', component: PageNotFoundComponent }
]
  • Routing by Template
<ul>
    <li><a [routerLink]="['/welcome']">Home</a></li>
    <li><a [routerLink]="['/products']">Product List</a></li>
</ul>

<ul>
    <li><a routerLink="/welcome">Home</a></li>
    <li><a routerLink="/products">Product List</a></li>
</ul>

<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
<a [routerLink]="['/products', product.id, 'edit']">Edit</a>
<a [routerLink]="['/products', 0, 'edit']">Add Product</a>
<a routerLink="/products/0/edit">Add Product</a>
<a [routerLink]="['/products', {name: productName, code: productCode,startDate: availabilityStart, endDate: availabilityEnd}]">Any Text</a>
  • Routing by Code
this.router.navigate(['/welcome']);// Standard syntax
this.router.navigate('/welcome');// Short-cut syntax
this.router.navigateByUrl('/welcome');// Complete Url path, used to remove any secondary route from nav.
this.router.navigate(['/products',this.product.id]); 
  • Grouping Routes
    • Routes not grouped
RouterModule.forChild([;
    {
        path: 'products',
        component: ProductListComponent
    },
    {
        path: 'products/:id',
        component: ProductDetailComponent,
        resolve: { product: ProductResolver}
    },
    {
        path: 'products/:id/edit',
        component: ProductEditComponent,
        resolve: { product: ProductResolver},
        children: []
    }
])
  • Routes grouped (recommended)
    • The routing shown below is associated with the component defined in the parent
    • Component-less routing is also available to allow child routes to display on a higher-level router outlet.
RouterModule.forChild([
    {
        path: 'products',
        component: ProductListComponent,
        children: [
            {
                path: ':id',
                component: ProductDetailComponent,
                resolve: { product: ProductResolver}
            },
            {
                path: ':id/edit',
                component: ProductEditComponent,
                resolve: { product: ProductResolver},
                children: []
            }
        ]
    }
])
  • Use Router Outlets 
    • Acts as a placeholder that Angular dynamically fills based on the current router state. You can have one or more of them.
<router-outlet name="images"></router-outlet>
<router-outlet name="details"></router-outlet>
<router-outlet name="popup"></router-outlet>

RouterModule.forChild([
    {
        path: 'messages',
        component: MessageComponent,
        outlet: 'popup' //Defining a specific outlet to show on.
    }
])
  • Use Route guards
    • canActivate: Called when the Url changes to the route 
      • Checks criteria before activating a route
      • Commonly used to: – 
        • Limit route access to specific users
        • Ensure prerequisites are met 
    • canActivateChild: Called when the Url changes to the childe route 
      • Checks criteria before activating a child route
      • Commonly used to: – 
        • Limit access to child routes
        • Ensure prerequisites for child routes are met
    • canDeactivate: Called when the Url changes to a different route
      • Checks criteria before leaving a route
      • Commonly used to: – 
        • Check for unsaved changes
        • Confirm leaving an incomplete operation
    • canLoad: Checks criteria before loading an async route
      • Commonly used to: – 
        • Prevent loading a route if a user cannot access it
  • Lazy loading: Loading a module when needed, delaying its load until then
{
    path: 'products',
    canActivate: [AuthGuard],
    data: { preload: false },
    loadChildren: () => import('./products/product.module#ProductModule').then(m => m.ProductModule)
  }

Router Store (Ngrx)

import { StoreModule } from '@ngrx/store';
import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store';
     
 @NgModule({
   imports: [
     BrowserModule,
     StoreModule.forRoot({router: routerReducer}),
      StoreRouterConnectingModule
   ],
   bootstrap: [ AppComponent ]
 })
 export class AppModule {
 }

State management

NgRx is redux pattern + Angular for state management.

Redux Principles

  • A single source of truth called the store.
  • The state is read-only and only changed by dispatching actions.
  • Changes are made using pure functions called reducers.

What Should NOT Go in the Store?

  • Unshared State
  • Angular form State
  • Non-serializable state

State management components

  • Store: 
    • There is only one store in redux, this represents the application state.
    • The state is read-only and changed by dispatching actions.
    • Install @ngrx/store package
    • Organize application state by feature
    • Name the feature slice with the feature name
    • Initialize the store using:
StoreModule.forRoot(reducer)
StoreModule.forFeature('feature', reducer)
  • State should be maintained as immutable
    • Immutable vs. Mutable examples
state.products.push(action.payload) //Mutable, should not be used
state.products.concat(action.payload) //Immutable
[...state.products, action.payload] //Immutable
  • Action: 
    • Dispatch an action to invoke change in state.
    • An action represents an event.
    • Define an action for each event worth tracking.
    • Action is often done in response to a user action or an operation.
  • Effects: 
    • Actions with Side Effects, Manage side effects with the NgRx Effects library.
    • Effects Take Actions and Dispatch Actions.
      • Use case example: saving a user preference to the database; dispatches action to effect -> attempts to save preference -> dispatches success or failure action -> reducer saves to state if successful
    • Effects can Take Actions and Do Nothing (set @Effects({dispatch:false}))
      • Use case example: Logging information (fire and forget)
    • Effects Keep Components Pure.
    • Isolate side effects.
    • Easier to test
  • Reducers: 
    • Reducers act on action to take existing state and updates; it create a new copy of state.
    • Reducers Are Pure Functions.
    • Responds to dispatched actions.
    • Replaces the state tree with new state.
    • Build a reducer function (often one or more per feature).
    • Implement as a switch with a case per action.
  • Selectors 
    • Build selectors to define reusable state queries
      • Provide a strongly typed API
      • Decouple the store from the components
        • Any change in state structure will not require multiple changes 
      • Encapsulate complex data transformations
        • ability to pull information from multiple reducers without multiple subscribes
      • Reusable
      • Memoized: cached
    • Declaring a selector example: Feature selector && State selector
const getProductFeatureState = createFeatureSelector<ProductState>('products');
export const getShowProductCode = createSelector(
    getProductFeatureState,
    state => state.showProductCode
);
  • Using a selector example:
this.store.pipe(select(fromProduct.getShowProductCode)).subscribe(
    showProductCode => this.displayCode= showProductCode
);

Redux pattern advantages

  • Centralized immutable state
  • Performance
  • Testability
  • Tooling
  • Component communication

Registering the store module

  • Registering the state for its respective module – app module and feature module.
StoreModule.forRoot({}) //App module
StoreModule.forFeature('products', reducer); //Feature module
  • Subscribing in component
this.store.pipe(select('products'))
  • Registering an Effect – app module and feature modules respectively
EffectsModule.forRoot([]) //App module
EffectsModule.forFeature([ProductEffects]) //Feature module   

Redux Store Dev Tools

  • Install browser Redux DevTools extension
  • Install @ngrx/store-devtools
  • Initialize @ngrx/store-devtools module
  • Initializing Store Dev Tools
    • App Module 
import {StoreDevtoolsModule} from "@ngrx/store-devtools";
StoreDevtoolsModule.instrument({name: "Demo App DevTools", maxAge:25, logOnly: environment.production})

RxJS Operators

  • switchMap
    • Cancels the current subscription/request and can cause race condition
    • Use for getting requests or cancelable requests like searches
  • concatMap
    • Runs subscriptions/requests in order and is less performant
    • Use for getting, post and put requests when order is important
  • mergeMap
    • Runs subscriptions/requests in parallel
    • Use for put, post and delete methods when order is not important
  • exhaustMap
    • Ignores all subsequent subscriptions/requests until it completes
    • Use for login when you do not want more requests until the initial one is complete
    • An example application of the operator
@Injectable() 
export class ProductEffects { 
    constructor(private productService: ProductService, 
        private actions$: Actions) { }

    @Effect() 
    loadProducts$ = this.actions$.pipe(
        ofType(ProductActionTypes.Load), 
        mergeMap(action => 
            this.productService.getProducts().pipe(
                map(products => (new LoadSuccess(products))),
                catchError(err => of(new LoadFail(err))) //Exception handling
            )
        )
    ); 
}
  • Example calling service, map, and catch the error
@Effect()updateProduct$: Observable<Action>

updateProduct$: Observable<Action> = this.actions$.pipe(
    ofType(fromProduct.ProductActionTypes.UpdateProduct),
    map((action: fromProduct.UpdateProduct) =>action.payload), 
    mergeMap((product: Product) => 
        this.productService.updateProduct(product).pipe(
            map(updatedProduct=>(newfromProduct.UpdateProductSuccess(updatedProduct))), 
            catchError(err =>of(newfromProduct.UpdateProductFail(err)))
        )
    )
);

  • takeWhile
    • This operator can be used to unsubscribe from observable, In the example the componentActive is a component level variable declared as true, When ngOnDestroy you flip this to false leads an unsubscribe trigger in subscribed call. 
    • Example takeWhile
componentActive= true;

this.store.pipe(select(fromProduct.getProducts),
    takeWhile(() => this.componentActive)) //unsubscribes as soon as ngOnDestroy sets componentActive to false.
    .subscribe((products: Product[]) => this.products= products);
}

ngOnDestroy() {componentActive= false;}

Subscription

  • Component Subscription vs. AsyncPipe
    • Component Subscription
this.productService.getProducts()
    .subscribe(products => this.products= products);
  • Async Pipe
<div *ngIf="products$ | async">

If our components are pure then using async pipe will help a lot in fixing subscription

Application state structure example

{
  "app": {                      // application root reducer
    "hideWelcomePage": true
  },
  "products": {                 // product reducer will access this section
    "showProductCode": true,
    "currentProduct": {},
    "products": []
  },
  "users": {                    // user reducer slice of state
    "maskUserName": false,
    "currentUser": {}
  },
  "customers": {                // customer reducer slice of state
    "customerFilter": "Harkness",
    "currentCustomer": {},
    "customers": []
  }
}

Presentational vs Container Components

  • Presentational role
    • Concerned withhow things look
    • HTML markupand CSS styles
    • No dependencies on the rest of the app
    • Don’t specify how data is loaded or changed but emit events via @Outputs
    • Receive data via @Inputs
    • May contain other components (Presentational or Container)
  • Container role
    • Concerned with how things work
    • Have little to no HTML and CSS styles
    • Have injected dependencies
    • Are stateful and specify how data is loaded or changed
    • Top level routes
    • May contain other components (Presentational or Container)

State example with all components – removed imports and non-essential parts of code

  • app.state.ts
// Representation of the entire app state
// Extended by lazy loaded modules
export interface State {
    user: UserState;
}
  • app-routing.module.ts
const appRoutes: Routes = [
  {
    path: '',
    component: ShellComponent,
    children: [
      { path: 'welcome', component: WelcomeComponent },
      {
        path: 'products',
        loadChildren: './products/product.module#ProductModule'
      },
      { path: '', redirectTo: 'welcome', pathMatch: 'full' },
    ]
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }
  • app.module.ts
/* Feature Modules */
import { UserModule } from './user/user.module';

/* NgRx */
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { EffectsModule } from '@ngrx/effects';

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    HttpClientInMemoryWebApiModule.forRoot(ProductData),
    UserModule,
    AppRoutingModule,
    StoreModule.forRoot({}),
    StoreDevtoolsModule.instrument({
      name: 'Demo App',
      maxAge: 25,
      logOnly: environment.production,
    }),
    EffectsModule.forRoot([])
  ],
  declarations: [
    AppComponent,
    ShellComponent,
    MenuComponent,
    WelcomeComponent,
    PageNotFoundComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
  • app-shell.component.html
<pm-menu></pm-menu>

<div class='container main-content'>
    <router-outlet></router-outlet>
</div>
  • product feature – product-shell.component.html (container component with two presentational components)
<div class='row'>
  <div class='col-md-4'>
    <pm-product-list 
    [displayCode]="displayCode$ | async"
    [products]="products$ | async"
    [selectedProduct]="selectedProduct$ | async"
    [errorMessage]="errorMessage$ | async"
    (checked)="checkChanged($event)"
    (initializeNewProduct)="(newProduct())"
    (selected)="productSelected($event)"></pm-product-list>
  </div>
  <div class='col-md-8'>
    <pm-product-edit
    [selectedProduct]="selectedProduct$ | async"
    [errorMessage]="errorMessage$ | async"
    (clearCurrent)="clearProduct()"
    (update)="updateProduct($event)"
    (delete)="deleteProduct($event)"
    (create)="saveProduct($event)"></pm-product-edit>
  </div>
</div>
  • product feature – product-shell.component.ts
import * as fromProduct from './../../state';
import * as productActions from './../../state/product.actions';
import { Product } from '../../product';

@Component({
  templateUrl: './product-shell.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductShellComponent implements OnInit {
  displayCode$: Observable<boolean>;
  selectedProduct$: Observable<Product>;
  products$: Observable<Product[]>;
  errorMessage$: Observable<string>;

  constructor(private store: Store<fromProduct.State>) {}

  ngOnInit(): void {
    this.store.dispatch(new productActions.Load());
    this.products$ = this.store.pipe(select(fromProduct.getProducts));
    this.errorMessage$ = this.store.pipe(select(fromProduct.getError));
    this.selectedProduct$ = this.store.pipe(select(fromProduct.getCurrentProduct));
    this.displayCode$ = this.store.pipe(select(fromProduct.getShowProductCode));
  }

  checkChanged(value: boolean): void {
    this.store.dispatch(new productActions.ToggleProductCode(value));
  }

  productSelected(product: Product): void {
    this.store.dispatch(new productActions.SetCurrentProduct(product));
  }

  deleteProduct(product: Product): void {
    this.store.dispatch(new productActions.DeleteProduct(product.id));
  }    
}
  • product feature – index.ts
import { createFeatureSelector, createSelector, ActionReducerMap } from '@ngrx/store';
import * as fromRoot from '../../state/app.state';
import * as fromProducts from './product.reducer';

// Extends the app state to include the product feature.
// This is required because products are lazy loaded.
// So the reference to ProductState cannot be added to app.state.ts directly.
export interface State extends fromRoot.State {
    products: fromProducts.ProductState;
}

// Selector functions
const getProductFeatureState = createFeatureSelector<fromProducts.ProductState>('products');

export const getShowProductCode = createSelector(
    getProductFeatureState,
    state => state.showProductCode
);

export const getCurrentProductId = createSelector(
    getProductFeatureState,
    state => state.currentProductId
);

export const getCurrentProduct = createSelector(
    getProductFeatureState,
    getCurrentProductId,
    (state, currentProductId) => {
        if (currentProductId === 0) {
            return {
                id: 0,
                productName: '',
                productCode: 'New',
                description: '',
                starRating: 0
            };
        } else {
            return currentProductId ? state.products.find(p => p.id === currentProductId) : null;
        }
    }
);
  • product feature – product.actions.ts
import { Product } from '../product';

/* NgRx */
import { Action } from '@ngrx/store';

export enum ProductActionTypes {
  ToggleProductCode = '[Product] Toggle Product Code',
  SetCurrentProduct = '[Product] Set Current Product',
  ClearCurrentProduct = '[Product] Clear Current Product',
  InitializeCurrentProduct = '[Product] Initialize Current Product',
  Load = '[Product] Load',
  LoadSuccess = '[Product] Load Success',
  LoadFail = '[Product] Load Fail',
  UpdateProduct = '[Product] Update Product',
  UpdateProductSuccess = '[Product] Update Product Success',
  UpdateProductFail = '[Product] Update Product Fail'
}

// Action Creators
export class ToggleProductCode implements Action {
  readonly type = ProductActionTypes.ToggleProductCode;

  constructor(public payload: boolean) { }
}

export class SetCurrentProduct implements Action {
  readonly type = ProductActionTypes.SetCurrentProduct;

  constructor(public payload: Product) { }
}

export class ClearCurrentProduct implements Action {
  readonly type = ProductActionTypes.ClearCurrentProduct;
}

export class InitializeCurrentProduct implements Action {
  readonly type = ProductActionTypes.InitializeCurrentProduct;
}

export class Load implements Action {
  readonly type = ProductActionTypes.Load;
}

export class LoadSuccess implements Action {
  readonly type = ProductActionTypes.LoadSuccess;

  constructor(public payload: Product[]) { }
}

export class LoadFail implements Action {
  readonly type = ProductActionTypes.LoadFail;

  constructor(public payload: string) { }
}

export class UpdateProduct implements Action {
  readonly type = ProductActionTypes.UpdateProduct;

  constructor(public payload: Product) { }
}

export class UpdateProductSuccess implements Action {
  readonly type = ProductActionTypes.UpdateProductSuccess;

  constructor(public payload: Product) { }
}

export class UpdateProductFail implements Action {
  readonly type = ProductActionTypes.UpdateProductFail;

  constructor(public payload: string) { }
}  

// Union the valid types
export type ProductActions = ToggleProductCode
  | SetCurrentProduct
  | ClearCurrentProduct
  | InitializeCurrentProduct
  | Load
  | LoadSuccess
  | LoadFail
  | UpdateProduct
  | UpdateProductSuccess
  | UpdateProductFail;
  • product feature – product.effects.ts
import { Injectable } from '@angular/core';

import { Observable, of } from 'rxjs';
import { mergeMap, map, catchError } from 'rxjs/operators';

import { ProductService } from '../product.service';
import { Product } from '../product';

/* NgRx */
import { Action } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';
import * as productActions from './product.actions';

@Injectable()
export class ProductEffects {

  constructor(private productService: ProductService,
              private actions$: Actions) { }

  @Effect()
  loadProducts$: Observable<Action> = this.actions$.pipe(
    ofType(productActions.ProductActionTypes.Load),
    mergeMap(action =>
      this.productService.getProducts().pipe(
        map(products => (new productActions.LoadSuccess(products))),
        catchError(err => of(new productActions.LoadFail(err)))
      )
    )
  );

  @Effect()
  updateProduct$: Observable<Action> = this.actions$.pipe(
    ofType(productActions.ProductActionTypes.UpdateProduct),
    map((action: productActions.UpdateProduct) => action.payload),
    mergeMap((product: Product) =>
      this.productService.updateProduct(product).pipe(
        map(updatedProduct => (new productActions.UpdateProductSuccess(updatedProduct))),
        catchError(err => of(new productActions.UpdateProductFail(err)))
      )
    )
  );
}
  • product feature – product.reducer.ts
import { Product } from '../product';
import { ProductActionTypes, ProductActions } from './product.actions';

// State for this feature (Product)
export interface ProductState {
  showProductCode: boolean;
  currentProductId: number | null;
  products: Product[];
  error: string;
}

const initialState: ProductState = {
  showProductCode: true,
  currentProductId: null,
  products: [],
  error: ''
};

export function reducer(state = initialState, action: ProductActions): ProductState {

  switch (action.type) {
    case ProductActionTypes.ToggleProductCode:
      return {
        ...state,
        showProductCode: action.payload
      };

    case ProductActionTypes.SetCurrentProduct:
      return {
        ...state,
        currentProductId: action.payload.id
      };

    case ProductActionTypes.ClearCurrentProduct:
      return {
        ...state,
        currentProductId: null
      };

    case ProductActionTypes.InitializeCurrentProduct:
      return {
        ...state,
        currentProductId: 0
      };

    case ProductActionTypes.LoadSuccess:
      return {
        ...state,
        products: action.payload,
        error: ''
      };

    case ProductActionTypes.LoadFail:
      return {
        ...state,
        products: [],
        error: action.payload
      };

    case ProductActionTypes.UpdateProductSuccess:
      const updatedProducts = state.products.map(
        item => action.payload.id === item.id ? action.payload : item);
      return {
        ...state,
        products: updatedProducts,
        currentProductId: action.payload.id,
        error: ''
      };

    case ProductActionTypes.UpdateProductFail:
      return {
        ...state,
        error: action.payload
      };  
    
    default:
      return state;
  }
}
  • product feature – product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable, of, throwError } from 'rxjs';
import { catchError, tap, map } from 'rxjs/operators';

import { Product } from './product';

@Injectable({
  providedIn: 'root',
})
export class ProductService {
  private productsUrl = 'api/products';

  constructor(private http: HttpClient) { }

  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.productsUrl)
      .pipe(
        tap(data => console.log(JSON.stringify(data))),
        catchError(this.handleError)
      );
  }

  createProduct(product: Product): Observable<Product> {
    const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
    product.id = null;
    return this.http.post<Product>(this.productsUrl, product, { headers: headers })
      .pipe(
        tap(data => console.log('createProduct: ' + JSON.stringify(data))),
        catchError(this.handleError)
      );
  }

  private handleError(err) {
    // in a real world app, we may send the server to some remote logging infrastructure
    // instead of just logging it to the console
    let errorMessage: string;
    if (err.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      errorMessage = `An error occurred: ${err.error.message}`;
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;
    }
    console.error(err);
    return throwError(errorMessage);
  }
}

Change Detection Strategy

  • OnPush
    • ChangeDetectionStrategy.OnPush means that the change detector’s mode will be initially set to CheckOnce
    • This is faster as it checks/detects its own hierarchy rather than the overall tree.
    • @Component({ templateUrl:"./product-shell.component.html", styleUrls:["./product-shell.component.css"], changeDetection: ChangeDetectionStrategy.OnPush }) export class ProductShellComponentimplements OnInit{}
  • Default
    • ChangeDetectionStrategy.Default is slower as it checks/detects each component.
    • @Component({ templateUrl:"./product-shell.component.html", styleUrls:["./product-shell.component.css"], changeDetection: ChangeDetectionStrategy.Default }) export class ProductShellComponentimplements OnInit{}

Barrel

  • A way to rollup exports from several modules into a single convenience module. The barrel itself is a module file that re-exports selected exports of other modules
    • app/index.ts
    export { Foo }from './app/foo'; export { Bar }from './app/bar'; export * as Baz from './app/baz';
    • app/product/consumer.ts
    import { Foo, Bar, Baz }from './app'; // index.ts implied by convention

Performance Points

  • trackBy
    • When using ngFor to loop over an array in templates, use it with a trackBy function which will return a unique identifier for each item.
    • When an array changes, Angular re-renders the whole DOM tree. Using trackBy, Angular will know which element has changed and will only make DOM changes for that particular element.// in the template <li *ngFor=”let item of items; trackBy: trackByFn”>{{ item }}</li> // in the component trackByFn(index, item) { return item.id; // unique id corresponding to the item }
  • let or const
    • use const if the value is never going to be reassigned for a variable
  • Subscribe in template
    • Avoid subscribing to observables from components and instead subscribe to the observables from the template.
    • Angular takes care of subscribing, unsubscribe
  • Use take, takeUntil, takeWhile operators
  • Use correct map operator eg switchMap, mergeMap, concatMap, exhaustMap, details are defined above in the doucment.
  • Lazy load modules
  • Build small reusable components, follow the single responsibility principle
  • Avoid logic in templates: Having logic in the template means that it is not possible to unit test it and therefore it is more prone to bugs when changing template code.

Leave a Reply

Your email address will not be published. Required fields are marked *