State Management in Angular - aakash14goplani/FullStack GitHub Wiki

Topics Covered


When to use Redux

Redux is not great for making simple things quickly. It’s great for making really hard things simple - Jani Eväkallio

In the official Redux docs you can read this (the same as for Redux/React apps applies to NgRX/Angular apps): "When should I use Redux?"

The need to use Redux should not be taken for granted. As Pete Hunt, one of the early contributors to React, says:

"You'll know when you need Flux. If you aren't sure if you need it, you don't need it."

Similarly, Dan Abramov, one of the creators of Redux, says:

"I would like to amend this: Don't use Redux until you have problems with vanilla React."

In general, use Redux when you have reasonable amounts of data changing over time, you need a single source of truth, and you find that approaches like keeping everything in a top-level React component's state are no longer sufficient.

However, it's also important to understand that using Redux comes with tradeoffs. It's not designed to be the shortest or fastest way to write code. It's intended to help answer the question "When did a certain slice of state change, and where did the data come from?", with predictable behavior. It does so by asking you to follow specific constraints in your application: store your application's state as plain data, describe changes as plain objects, and handle those changes with pure functions that apply updates immutably. This is often the source of complaints about "boilerplate". These constraints require effort on the part of a developer, but also open up a number of additional possibilities (such as store persistence and synchronization).

In the end, Redux is just a tool. It's a great tool, and there are some great reasons to use it, but there are also reasons you might not want to use it. Make informed decisions about your tools, and understand the tradeoffs involved in each decision.

References: When Should I use Redux, You might not need Redux - written by Redux creator Dan Abramov


Application State

  • A state is just the representation of your data at a given time i.e. status of your data at time t.

  • Example: You've an application in which a service would be there to manage the core data. You have multiple components that produces or utilized that data. This data is important for your application as it is controlling what's visible on the screen, that is your application state and this application state is lost whenever your application refreshes

  • You can resolve this by adding a backend service which is persistent and stores the state of your application at regular intervals. Each time your app loads or gets refreshed it could pull the data from persistent service and display that to user.

  • Now what's the problem with this approach? Simpler / smaller apps are fine with how we already manage state, by using components and services. If you have an app that becomes bigger and bigger and your state depends on many components at the same time and many components depend on the service and so on, then you could end up in state management nightmare.

  • It's hard to maintain the app because it's hard for you to tell where exactly am I managing this piece of information and maybe even changing a piece of information could accidentally break your code.

  • One solution is to use RxJs: Observables that allows us to create a streamlined state management experience. With RxJS:

    • we can react to user events or to application events like some data fetching
    • we can react to state changing event like an event where we want to update some data or some information in our app by using observables
    • we can emit or next a new data piece there and maybe use operators to even transform data in the way we want it and then listen to such state changes in other parts of the application where we need it to then update the UI.

NgRx Redux

  • The RxJS driven approach has some issues like:

    • your state can be updated from anywhere because you maybe failed to set up a clear flow of data.
    • your application state is mutable i.e. there might be cases where your state is intended to change, your code in there might not update state of data like example if you only change a property of an object, the overall object didn't change and therefore such a state change might not get picked up.
    • handling side effects i.e. things like HTTP requests, it's unclear where this should happen - should you write the code for sending them in a component? Should you do it in a service?
    • It does not enforces a specific strict pattern which we should follow to avoid above problems
  • That is where NgRx: Redux will be helpful. Redux is a state management pattern, it's also a library that helps you implement that pattern into any application.

  • The idea behind Redux is that you have one central store in your entire application that holds your application state. So think of that as a large JavaScript object that contains all the data and different services and components, can still interact with each other but they receive their state from that store, so that store is the single source of truth that manages the entire application state.

  • State is not just received, you sometimes also need to change the state, and for that in the Redux world, you dispatch so-called actions. An action is the end also just a Javascript object with an identifier basically identifying the kind of action you want to perform and optionally if that action needs some extra data to complete then you can have that data in the action that is supported by Redux.

  • Now that action doesn't directly reach the store, instead it reaches a so-called reducer. Now a reducer is just a function that gets the current state which is stored in the store and the action as a input, passed in automatically by the Redux library.

  • You can have a look at the action identifier to find out which kind of action it is, for example add a recipe or delete the recipe and then perform code on the state which you also got as an argument to update that state, however in an immutable way i.e. by copying it and then changing the copy because ultimately, the reducer returns a new state, so it returns a copy of the old state that will have been changed according to the action and that state which is returned by the reducer then is forwarded to the application store where this reduced state, again the state was edited immutably, so without touching the old state instead copying it and then editing the copy, so where this reduced state is then overwriting the old state of the application.

Redux Flow

  • That is the Redux pattern and as you can see, it enforces a pretty clean and clear flow of data. We only have one place in the app where we store our data and we only edit it through actions and reducers and we received the state through subscriptions which we can setup.

  • Now if you were using Angular, you could also use the Redux library, it's not restricted to be used with ReactJS only but NgRx is in the end just Angular's implementation of Redux.

  • NgRx for Redux comes with

    • injectable services so that you can easily access your application store in any part of your app by simply injecting it.
    • it also embraces RxJS and observables, so all the state is managed as one large observable
    • it handles side effects i.e. helps in managing HTTP requests

NgRx flow

Point to Remember In Redux, you only may execute synchronous code, so you must not and you can't send any asynchronous HTTP requests from inside a reducer function.


Creating Reducer

  • Installing core NgRx package: npm install @ngrx/store

  • Reducer is a simple function that takes initial state and action to be performed on state as input.

    export function shoppingListReducer(state, action) {}
  • Initial state is basically data present when the app loads initially. We could have some default values stored in an array as initial state of data:

    const initialState = {
       ingredients: [
         new Ingredients('paneer', 4),
         new Ingredients('paratha', 1),
         new Ingredients('mix-veggies', 4),
         new Ingredients('spices', 3)
      ]
    };
  • We can assign this initial state to reducer function

    export function shoppingListReducer(state = initialState, action) {}
    • This is ES6 syntax wherein you can assign a default/initial value to an function parameter if it is set to empty or null.
  • Now we need to perform operations based on input action type. So we can associate action to type Action from @ngrx/store and perform multiple operations based on action type.

    • Action should always be performed on copy state and not the original state and after performing operation we should always return the updated state.
    import { Action } from '@ngrx/store';
    ...
    export function shoppingListReducer(state = initialState, action: Action) {
       switch (action.type) {
         case 'ADD_INGREDIENT': 
             return {
                 ...state,
                 ingredients: [...state.ingredients, action]
             };
       }
    }

Adding Action

  • Actions always needs to be dispatched to the Reducer with certain identifier that states the type of action that needs to be performed on State and optionally a payload i.e. additional data that will be required to update state.

  • So we need to first define our Action with all required parameters. In the newly created file, the first step would be to export all identifiers. In our example that will be 'ADD_INGREDIENT':

    export const ADD_INGREDIENT = 'ADD_INGREDIENT';

    and make adjustments in the Reducer accordingly:

    import { ADD_INGREDIENT } from './shopping-list.actions';
    ...
    case 'ADD_INGREDIENT': ...
  • The second step is to export something that describes our Action. So here we can create a class AddIngredient, this name specifies what our Action actually does and implement the interface Action from @ngrx/store

    export class AddIngredient implements Action {}
  • The action interface forces you to add variable type of string. This will act as the identifier of your action. You can append this with readonly property which ensures that this is read-only and cannot be over-written.

  • We can add second property called payload that will hold the additional data required to update state.

    export class AddIngredient implements Action {
     readonly type: string = ADD_INGREDIENT;
     payload: Ingredients;
    }

Adding NgRx Store

  • Fix the imports as per newly prepared Action

    • Since we are exporting more than 1 thing in action class which needs to be imported here, look mat the syntax of multi-export, here ShoppingListActions will act as object that holds all the exports.
    • The action: Action will be changed to action: ShoppingListActions.AddIngredient to specify our custom action
    • case case 'ADD_INGREDIENT' will be changed to case ShoppingListActions.ADD_INGREDIENT
    • When we add ingredients to the array, we have new property payload in our Action that deals with additional data to be handled, so action will be replaced with action.payload
    import { Ingredients } from '../../shared/ingredients.model';
    import * as ShoppingListActions from './shopping-list.actions';
    ...
    export function shoppingListReducer(state = initialState, action: ShoppingListActions.AddIngredient) {
       switch (action.type) {
          case ShoppingListActions.ADD_INGREDIENT: 
             return {
                 ...state,
                 ingredients: [...state.ingredients, action.payload]
             };
       }
    }
  • Now to add Store, we go to app.module.ts file and import StoreModule from @ngrx/store. This module

    import { StoreModule } from '@ngrx/store';
  • We later add this to imports section specifying which Reducers are involved so-for using valid JavaScript object.

    StoreModule.forRoot({shoppingList: shoppingListReducer})
    • Here shoppingList is the label/key describing the Reducer
    • Here shoppingListReducer is the name of our Reducer
  • With this, NgRx will take that reducer into account and set up an application store for us where it registers this reducer, so now any actions that are dispatched will reach that reducer.


Selecting State

  • In shopping-list-component, we first create instance of Store from @ngrx/store:

    constructor(
      private shoppingListService: ShoppingListService,
      private store: Store<{ shoppingList: { ingredients: Ingredients[] } }>
    ) { }
    • here we are supposed to provide key which will be the label we used in app.module.ts while defining Store module StoreModule.forRoot({shoppingList: shoppingListReducer})
    • its type will be the label we used for defining initial state i.e. const initialState = { ingredients: [] }
  • Now we define a variable that will hold data return from state

    ingredientsModelArray: Observable<{ ingredients: Ingredients[] }>;
    ngOnInit(): void {
       this.ingredientsModelArray = this.store.select('shoppingList');
    }
  • As the state returns Observable, we can make use of async pipe to fetch details. Here the async will to return the state object which in turn has property ingredients which can be used to loop and fetch values

    <a class="list-group-item" style="cursor: pointer" *ngFor="let ingredient of (ingredientsModelArray | async).ingredients; let i = index" (click)="onEditIngredient(i)">
       {{ ingredient.name }} ({{ ingredient.amount }})
    </a>
  • Finally we add a default case to our Reducer so that it return initial state when there is nothing in place to return. When the app gets loaded first time, action defaults to "@ngrx/store/init"

    export function shoppingListReducer(state = initialState, action: ShoppingListActions.AddIngredient) {
       switch (action.type) {
          case ShoppingListActions.ADD_INGREDIENT: 
             return {
                ...state,
                ingredients: [...state.ingredients, action.payload]
             };
          default: return initialState;
       }
    }

Dispatching Action

  • We need to dispatch actions to the places that modify state of data like add/update. So we edit shopping-edit-component that deals with insertion and updating of data

  • We first create instance of Store

    import { Store } from '@ngrx/store';
    import * as ShoppingListActions from '../store/shopping-list.actions';
    ...
    constructor(
      private shoppingListService: ShoppingListService,
      private store: Store<{ shoppingList: { ingredients: Ingredients[] } }>
    ) { }
  • While adding ingredients we dispatch the Action

    this.store.dispatch(new ShoppingListActions.AddIngredient(new Ingredients(name, Number(amount))));
  • For above statement to work we need to modify our Action such that the payload property should now be accepted as constructor so that we can pass Ingredients as parameter like above. Note it should be public else it won't be accessible.

    export class AddIngredient implements Action {
      readonly type: string = ADD_INGREDIENT;
      constructor (public payload: Ingredients) {}
    }

Dispatching Multiple Actions

  • A use case we can add multiple ingredients i.e. dispatching multiple actions.

  • First step will be to modify our Action so that it can accept multiple inputs:

    export const ADD_INGREDIENTS = 'ADD_INGREDIENTS';
    ...
    export class AddIngredients implements Action {
       readonly type = ADD_INGREDIENTS;
       constructor (public payload: Ingredients[]) { }
    }
    ...
    export type ShoppingListActions = AddIngredient | AddIngredients;
    • Pay Attention: we have removed string type from our const because
      export const ADD_INGREDIENTS: string = 'ADD_INGREDIENTS';
      /* changes to */
      export const ADD_INGREDIENTS = 'ADD_INGREDIENTS';
      /* other valid option */
      export const ADD_INGREDIENTS: 'ADD_INGREDIENTS' = 'ADD_INGREDIENTS';
  • Second step is to update Reducer,

    export function shoppingListReducer(state = initialState, action: ShoppingListActions.ShoppingListActions) {
       switch (action.type) {
       ...
       case ShoppingListActions.ADD_INGREDIENTS: 
       return {
          ...state,
          ingredients: [...state.ingredients, ...action.payload]
       };
    ...
    • Above case statement will result into error. The reason is we tell TypeScript that the action type of the action we're getting in the reducer is ShoppingListActions ADD_INGREDIENTS.
    • Now that was true when we only had only one action but now we have two actions ShoppingListActions ADD_INGREDIENT and ShoppingListActions ADD_INGREDIENTS and we can't tell whether the action that reaches this reducer is actually the ADD_INGREDIENT or the ADD_INGREDIENTS action.
    • To resolve this, In Actions, we export a union type export type ShoppingListActions = AddIngredient | AddIngredients;
    • Now action.payload here should not be added like this because it will be an array of ingredients, so if we add it like this, then we add a array to an array and hence we have a nested array. I want to add the elements of that payload array to the outer array and therefore here, we should also use the spread operator to pull these elements in action.payload out of this array and add them to this ingredients array.
  • Last step is to dispatch action in our service

    import { Store } from '@ngrx/store';
    import * as ShoppingListActions from '../shopping-list/store/shopping-list.actions';
    ...
    constructor(
         private shoppingListService: ShoppingListService,
         private store: Store<{ shoppingList: { ingredients: Ingredients[] } }>
    ) {}
    ...
    this.store.dispatch(new ShoppingListActions.AddIngredients(ingredientsArray));

Updating and Deleting Items

  • First step is to modify actions to include identifiers for both the actions

    export const UPDATE_INGREDIENTS = 'UPDATE_INGREDIENTS';
    export const DELETE_INGREDIENTS = 'DELETE_INGREDIENTS';
    ...
    export class UpdateIngredients implements Action {
       readonly type = UPDATE_INGREDIENTS;
       constructor (public payload: {index: number, ingredient: Ingredients}) { }
    }
    export class DeleteIngredients implements Action {
       readonly type = DELETE_INGREDIENTS;
       constructor (public payload: number) { }
    }
    ...
    export type ShoppingListActions = AddIngredient | AddIngredients | UpdateIngredients | DeleteIngredients;
  • Then modify reducer

    case ShoppingListActions.UPDATE_INGREDIENTS:
       // fetch current ingredient
       const oldIngredient = state.ingredients[action.payload.index];
       // immutable change - create new object, copy existing data, add new data from payload so the updatedData consist data to be updated
       const updatedIngredient = {
          ...oldIngredient,
          ...action.payload.ingredient
       };
       // we need array in template, fetch existing ingredients
       const updatedIngredients = [...state.ingredients];
       // update array with new ingredient
       updatedIngredients[action.payload.index] = updatedIngredient;
          return {
             ...state,
             ingredients: updatedIngredients
          };
    
    case ShoppingListActions.DELETE_INGREDIENTS:
       return {
          ...state,
          // filter returns a new array with filtered items
          ingredients: state.ingredients.filter((currentIngredient, currentIngredientIndex) => {
             return currentIngredientIndex !== action.payload;
          })
       };
  • Finally Dispatch the actions

    this.store.dispatch(new ShoppingListActions.UpdateIngredients({ index: this.editedIndexNumber, ingredient: new Ingredients(name, Number(amount)) }));
    ...
    this.store.dispatch(new ShoppingListActions.DeleteIngredients(this.editedIndexNumber));

Type Definition for State

  • Initial State is:

    const initialState = {
      ingredients: [
         new Ingredients('paneer', 4),
         new Ingredients('paratha', 1),
         new Ingredients('mix-veggies', 4),
         new Ingredients('spices', 3)
      ]
    };
    • This state does not have any type definition also if we add any new properties to initial state we need to make update to multiple component and services where we are using this state.
  • To make it more verbose we should add a type definition to our state at one place and use that everywhere. One advantage would be if we want to change the contents within state, we need to update that only in one place and not across the project.

  • For this we create an interface and assign that as type to the initial state:

    export interface State {
      ingredients: Ingredients[];
      editedIngredient: Ingredients;
      editedIngredientIndex: number;
    }
    ...
    const initialState: State = { ... }
  • As of now we just have one state in our project and that is also defined in our app.module

    StoreModule.forRoot({shoppingList: shoppingListReducer})
    • In future we could have many such states associated, so we can create one common application wide interface to handle type definitions if all state (be careful with the name, use same name as used in app.module)
      export interface AppState {
         shoppingList: State
      }
  • We can later use this across our components and services:

    // below initial snippet
    private store: Store<{ shoppingList: { ingredients: Ingredients[] } }>
    // changes to...
    import * as fromShoppingList from '../store/shopping-list.reducer';
    ...
    private store: Store<fromShoppingList.AppState>

Subscribing and Pushing Data to State

/* subscribing from service */
this.subscription = this.shoppingListService.ingredientEdit.subscribe((index: number) => {
   this.editedIndexNumber = index;
   this.editMode = true;
   this.editedItem = this.shoppingListService.getIngredient(index);
});

/* subscribing to state */
this.subscription = this.store.select('shoppingList').subscribe(stateData => {
   if (stateData.editedIngredientIndex > -1) {
      this.editedIndexNumber = stateData.editedIngredientIndex;
      this.editMode = true;
      this.editedItem = stateData.editedIngredient;
   }
});

/* pushing data to service */
this.shoppingListService.ingredientEdit.next(id);

/* pushing data to state */
this.store.dispatch(new ShoppingListActions.StartEdit(id));

/* unsubscribe */
ngOnDestroy(): void {
   this.subscription.unsubscribe();
   this.store.dispatch(new ShoppingListActions.StopEdit());
}

Creating One Root State for Entire Application

Initial Project had just one State and one corresponding declaration in App Module file

/* State */
export interface State {
    ingredients: Ingredients[];
    editedIngredient: Ingredients;
    editedIngredientIndex: number;
}
...
/* App Module declaration */
StoreModule.forRoot({
   shoppingList: shoppingListReducer
})

When the number of states increases, corresponding entries also increases

/* State 1: within shopping-list.reducer.ts */
export interface State {
    ingredients: Ingredients[];
    editedIngredient: Ingredients;
    editedIngredientIndex: number;
}
/* State 2: within auth.reducer.ts */
export interface State {
    user: User;
}
...
/* App Module declaration */
StoreModule.forRoot({
   shoppingList: shoppingListReducer,
   auth: authReducer
})

This increases overall complexity and makes it tough to maintain them as well. We can create a single state application wide that will control states of all components and services. For this

  1. We first create a separate file and define interface that hold the state of all the components and services
    import * as fromShoppingList from '../shopping-list/store/shopping-list.reducer';
    import * as fromAuth from '../auth/store/auth.reducer'
    import { ActionReducerMap } from '@ngrx/store';
    
    export interface AppState {
       shoppingList: fromShoppingList.State,
       auth: fromAuth.State
    }
    
    export const appReducer: ActionReducerMap<AppState> = {
       shoppingList: fromShoppingList.shoppingListReducer,
       auth: fromAuth.authReducer
    };
  2. We then create a Reducer of type ActionReducerMap<T> and pass-in our interface as its type. This reducer is basically an object that needs list of reducers in our application. Here key is the thing that you defined in your AppState interface and its value is the corresponding Reducers associated with those components/services
  3. We then update our App Module with newly created Reducer type
    import * as fromAuth from './store/app.reducer';
    ...
    StoreModule.forRoot(fromAuth.appReducer)
  4. We then use this particular state in all our components and services
    import * as fromApp from '../store/app.reducer';
    ...
    constructor(
       private store: Store<fromApp.AppState>
    ) { }
    ngOnInit(): void {
       this.ingredientsModelArray = this.store.select('shoppingList');
       this.authentication = this.store.select('auth');
    }

Note on using Actions

  • When NgRx starts up, it sends one initial action to all reducers and since this action has an identifier we don't handle anywhere, we make it into the default case, therefore we return the state and since we have no prior state when this first action is emitted, we therefore take this initial state.

  • This initial action dispatched automatically reaches all reducers. Well that is not just a case for the initial action, any action you dispatch, even your own ones, not just that initial one, so any action you dispatch by calling dispatch always reaches all reducers.

  • It does not just reach the authReducer because you are dispatching it in the authService, how would NgRx know that this is your intention? It would be dangerous if that would be the result because that means you could never reach another reducer with actions dispatched here in the authService and there certainly are scenarios where you might want to reach another reducer. So therefore, any action you dispatch always reaches all reducers.

  • This has an important implication, it's even more important that you always copy the old state here and return this because if a shoppingList related action is dispatched, it still reaches the authReducer. In there (authReducer) of course, we have no case that would handle that action, hence all these code snippets here don't kick in but therefore the default case kicks in and handles this and there, we absolutely have to return our current state, otherwise we would lose that or cause an error.

  • The next important thing is that since our dispatched actions reach all reducers, you really have to ensure that your identifiers here are unique across the entire application because these auth-related actions here are not just handled by the authReducer, they also reach the shoppingListReducer.

  • General Syntax of writing any action

    /* used in this guide */
    export const LOGIN = 'LOGIN';
    export const ADD_INGREDIENT = 'ADD_INGREDIENT';
    /* NgRx recommended approach '[Action Name] Identifier' */
    export const LOGIN = '[AUTH] Login';
    export const ADD_INGREDIENT = '[Shopping List] Add Ingredient';

NgRx Side Effects

  • Side effects are basically parts in your code where you run some logic but that's not so important for the immediate update of the current state. For example here, the HTTP request

  • To work with these side effects there is a separate package - @ngrx/effects that gives us tools for elegantly working with side effects between actions which we dispatch and receive so that we can keep our reducers clean and still have a good place for managing these side effects.


Creating Side Effect

  • Create a file auth.effects.ts that exports a class with same name as file name

    export class AuthEffects {}
  • We need to inject dependency Actions (previously we used Action from @ngrx/store) that helps us to create a stream of actions which we need to handle async request

    import { Actions, ofType } from '@ngrx/effects';
    @Injectable()
    export class AuthEffects {
      constructor (
         private actions$: Actions
      ) {}
    }
    • Here $ behind action variable is a notation to specify that given variable/property is an Observable
    • We need to add @Injectable() as later on we will add http and routing dependencies via constructor.
  • We create a property that would basically deal with handling side effect. Since we are working on HTTP login effect, we create a property authLogin

  • In our actions file, we export a new action type LOGIN_START that will mark as an async http action has just begun & LOGIN_FAIL action will deal with error cases.

  • We also add corresponding classes for these actions. LoginStart needs credentials in payload to create user object and complete login process and LoginFail needs a message that can be displayed as error message on screen

    export const LOGIN_START = '[Auth] Login Start';
    export const LOGIN_FAIL = '[Auth] Login Fail';
    ...
    export class LoginStart implements Action {
       readonly type = LOGIN_START;
       constructor( public payload: { email: string, password: string }) {}
    }
    export class LoginFail implements Action {
       readonly type = LOGIN_FAIL;
       constructor( public payload: string ) {}
    }
    ...
    export type AuthActions = Login | Logout | LoginStart | LoginFail;
  • Update corresponding Action handling in our Reducer as well. When Login starts, we need loading spinner and error is null at this stage. If Login fails, we stop loading and display error message.

    case AuthActions.LOGIN_START:
       return {
         ...state,
         authError: null,
         loading: true
       }
    case AuthActions.LOGIN_FAIL: 
       return {
         ...state,
         authError: action.payload,
         loading: false
    }
  • Add import to app.module file

    imports: [
       EffectsModule.forRoot([AuthEffects])
    ],
  • Back to our effects file, we now listen to newly created action from our stream

    @Effect()
    authLogin = this.actions$.pipe(
        ofType(AuthActions.LOGIN_START)
    );
    • We should not subscribe to actions$, Angular does that for us, we should attach pipe() and perform other operations
    • ofType() is an operator provided by @ngrx/effects that help us to listen to filtered actions only.
    • We can provide multiple comma separated actions as well...
    • @Effect() is ngrx special decorator to let compiler know we are creating an effect here
  • We now make HTTP calls within our effect.

    @Effect()
    authLogin = this.actions$.pipe(
       ofType(AuthActions.LOGIN_START),
         switchMap((authData: AuthActions.LoginStart) => {
            return this.http
            .post<AuthResponsePayload>('url')
            .pipe(
               map(responseData => {
                  // response handling
                  return new AuthActions.Login({email, id, token, expirationDate});
               }),
               catchError(errorRes => {
                  // error handling
                  return of(new AuthActions.LoginFail(errorMessage));
               })
            );
        })
     );
    • Effect will only start when we have Action of type Login for Auth.
    • It will process the request and will either yield result or throw error. In either case we need to return observable.
    • The map() method of rxjs returns an observable of whatever you process within it.
    • For catchError() you need to wrap message inside observable by creating new observable using of()
    • This observable - pass/fail will be passed to switchMap() which will return this to authLogin that can be later used in our services and components to carry out appropriate operations.
  • The reason for returning observable from switchMap() via catchError() and map() is that:

    • effects here is an ongoing observable stream, this must never die, at least not as long as our application is running and therefore, if we would catch an error here by adding catchError() as a next step after switchMap(), this entire observable stream will die.
    • which means that trying to login again will simply not work because this effect here will never react to another dispatched login start event (app will not respond, user will keep on hitting login button but nothing will happen) because this entire observable is dead and therefore, errors have to be handled on a different level.
    • most important - we have to return a non-error observable so that our overall stream doesn't die and since switchMap() returns the result of this inner observable stream as a new observable to the outer chain here, returning a non-error observable in catchError() is crucial, so that this erroneous observable in here still yields a non-error observable which is picked up by switchMap() which is then returned to this overall stream, to this overall observable chain.
  • We then dispatch these actions

    this.store.dispatch( new AuthActions.LoginStart({email, password}) );
  • After successful login we want user to navigate to home page. Important - Navigation are also kind of side effects and must be handled accordingly

    @Effect({ dispatch: false })
    authSuccess = this.actions$.pipe(
       ofType(AuthActions.LOGIN),
       tap(() => {
          this.router.navigate(['/']);
       })
    );
    • this is an effect which does not dispatch a new action at the end. Your effects do that, they typically return an observable which holds a new effect which should be dispatched, but this effect doesn't and to let NgRx effect know about that and avoid errors, you have to pass an object dispatch: false to your @Effect() decorator.
    • Once the Login Action is successful, user will navigate to desired path!

Redux Dev Tool

  • Install 2 packages as dev dependencies: store-devtools and router-store

  • In app.module, update following configurations:

    import { StoreDevtoolsModule } from '@ngrx/store-devtools';
    import { StoreRouterConnectingModule } from '@ngrx/router-store';
    ...
    imports: [
      StoreDevtoolsModule.instrument({ logOnly: environment.production }),
      StoreRouterConnectingModule.forRoot()
    ],
  • Download Redux DevTools plugin from Google store and restart browser. After restarting, In Developers tools you'll see new option Redux


Alternate Syntax

Recently the official NgRX docs have completely changed. Now they don't mention the traditional syntax used by Max any longer. They have completely switched to NgRX's new createAction()/createReducer()/createEffect() syntax, even though the "old" syntax is still fully working and not deprecated (you will find the "old" syntax here.

This change came very surprisingly, and with a delay after the Angular 8 release which has been the basis for Max' course updates.

With the new syntax we can avoid some of the traditional boilerplate code, and the actions can be easier tracked in the whole application (with the traditional approach we have to use the actions' string types in the reducers and the effects' ofType(), but the TS types of the actions themselves in the other parts of an app).

In spite of this change the structure of the code remains exactly the same (since it's the Redux pattern in the end), but it might be very confusing at first sight when you want to compare the course code with what you will find in the new docs.

Here is a description how you can transform the final code of this section into the new syntax, exactly preserving the app's functionality. More features (like createSelector(), createFeatureSelector(), createEntityAdapter() etc.) can be found in the NgRX docs, but these are beyond the scope of this course which is about Angular, not NgRX in depth.

If you want to switch to the new syntax (even though the old one is still valid) ...

  • replace the action files by these ones:

auth.actions.ts

import {createAction, props} from '@ngrx/store';
 
export const loginStart = createAction('[Auth] Login Start', props<{ email: string; password: string }>());
export const signupStart = createAction('[Auth] Signup Start', props<{ email: string; password: string }>());
export const authenticateSuccess = createAction('[Auth] Authenticate Success', props<{ email: string; userId: string; token: string; expirationDate: Date; redirect: boolean }>());
export const authenticateFail = createAction('[Auth] Authenticate Fail', props<{ errorMessage: string }>());
export const clearError = createAction('[Auth] Clear Error');
export const autoLogin = createAction('[Auth] Auto Login');
export const logout = createAction('[Auth] Logout');

recipe.actions.ts

import { createAction, props } from '@ngrx/store';
import { Recipe } from '../recipe.model';
 
export const addRecipe = createAction('[Recipe] Add Recipe', props<{ recipe: Recipe }>());
export const updateRecipe = createAction('[Recipe] Update Recipe', props<{ index: number, recipe: Recipe }>());
export const deleteRecipe = createAction('[Recipe] Delete Recipe', props<{ index: number }>());
export const setRecipes = createAction('[Recipe] Set Recipes', props<{ recipes: Recipe[] }>());
export const fetchRecipes = createAction('[Recipe] Fetch Recipes');
export const storeRecipes = createAction('[Recipe] Store Recipes');

shopping-list.actions.ts

import { createAction, props } from '@ngrx/store';
import { Ingredient } from '../../shared/ingredient.model';
 
export const addIngredient = createAction('[Shopping List] Add Ingredient', props<{ ingredient: Ingredient }>());
export const addIngredients = createAction('[Shopping List] Add Ingredients', props<{ ingredients: Ingredient[] }>());
export const updateIngredient = createAction('[Shopping List] Update Ingredient', props<{ ingredient: Ingredient }>());
export const deleteIngredient = createAction('[Shopping List] Delete Ingredient');
export const startEdit = createAction('[Shopping List] Start Edit', props<{ index: number }>());
export const stopEdit = createAction('[Shopping List] Stop Edit');
  • replace the reducer files by these ones:

auth.reducer.ts

import { Action, createReducer, on } from '@ngrx/store';
import { User } from '../user.model';
import * as AuthActions from './auth.actions';
 
export interface State { user: User; authError: string; loading: boolean; }
const initialState: State = { user: null, authError: null, loading: false };
 
export function authReducer(authState: State | undefined, authAction: Action) {
  return createReducer(
    initialState,
    on(AuthActions.loginStart, AuthActions.signupStart, state => ({...state, authError: null, loading: true})),
    on(AuthActions.authenticateSuccess, (state, action) => ({ ...state, authError: null, loading: false, user: new User(action.email, action.userId, action.token, action.expirationDate)})),
    on(AuthActions.authenticateFail, (state, action) => ({  ...state, user: null, authError: action.errorMessage, loading: false})),
    on(AuthActions.logout, state => ({...state, user: null })),
    on(AuthActions.clearError, state => ({...state, authError: null })),
  )(authState, authAction);
}

recipe.reducer.ts

import { Action, createReducer, on } from '@ngrx/store';
import { Recipe } from '../recipe.model';
import * as RecipesActions from '../store/recipe.actions';
 
export interface State { recipes: Recipe[]; };
const initialState: State = { recipes: []};
 
export function recipeReducer(recipeState: State | undefined, recipeAction: Action) {
  return createReducer(
    initialState,
    on(RecipesActions.addRecipe, (state, action) => ({ ...state, recipes: state.recipes.concat({ ...action.recipe }) })),
    on(RecipesActions.updateRecipe, (state, action) => ({ ...state, recipes: state.recipes.map((recipe, index) => index === action.index ? { ...action.recipe } : recipe) })),
    on(RecipesActions.deleteRecipe, (state, action) => ({ ...state, recipes: state.recipes.filter((recipe, index) => index !== action.index) })),
    on(RecipesActions.setRecipes, (state, action) => ({ ...state, recipes: [...action.recipes] }))
  )(recipeState, recipeAction);
}

shopping-list.reducer.ts

import { Action, createReducer, on } from '@ngrx/store';
import { Ingredient } from '../../shared/ingredient.model';
import * as ShoppingListActions from './shopping-list.actions';
 
export interface State { ingredients: Ingredient[]; editIndex: number; }
const initialState: State = { ingredients: [new Ingredient('Apples', 5), new Ingredient('Tomatoes', 10)], editIndex: -1 };
 
export function shoppingListReducer(shoppingListState: State | undefined, shoppingListAction: Action) {
  return createReducer(
    initialState,
    on(ShoppingListActions.addIngredient, (state, action) => ({ ...state, ingredients: state.ingredients.concat(action.ingredient) })),
    on(ShoppingListActions.addIngredients, (state, action) => ({ ...state, ingredients: state.ingredients.concat(...action.ingredients) })),
    on(ShoppingListActions.updateIngredient, (state, action) => ({ ...state, editIndex: -1, ingredients: state.ingredients.map((ingredient, index) => index === state.editIndex ? { ...action.ingredient } : ingredient) })),
    on(ShoppingListActions.deleteIngredient, state => ({ ...state, editIndex: -1, ingredients: state.ingredients.filter((ingredient, index) => index !== state.editIndex) })),
    on(ShoppingListActions.startEdit, (state, action) => ({ ...state, editIndex: action.index })),
    on(ShoppingListActions.stopEdit, state => ({ ...state, editIndex: -1 }))
  )(shoppingListState, shoppingListAction);
}
  • change these places in the effect files:

auth.effects.ts

  authSignup$ = createEffect(() =>
    this.actions$.pipe(
      ofType(AuthActions.signupStart),
      switchMap(action => {
              ...
              email: action.email,
              password: action.password,
              ...
  );
 
  authLogin$ = createEffect(() =>
    this.actions$.pipe(
      ofType(AuthActions.loginStart),
      switchMap(action => {
              ...
              email: action.email,
              password: action.password,
              ...
  );
 
  authRedirect$ = createEffect(() =>
    this.actions$.pipe(
      ofType(AuthActions.authenticateSuccess),
      tap(action =>  action.redirect && this.router.navigate(['/']))
    ), { dispatch: false }
  );
 
  autoLogin$ = createEffect(() =>
    this.actions$.pipe(
    ofType(AuthActions.autoLogin),
    ...
  );
 
  authLogout$ = createEffect(() =>
    this.actions$.pipe(
      ofType(AuthActions.logout),
      ...
    { dispatch: false }
  );

recipe.effects.ts

  fetchRecipes$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RecipesActions.fetchRecipes),
	  ...
  );
 
  storeRecipes$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RecipesActions.storeRecipes),
	  ...
    { dispatch: false }
  );
  • make a global search (Ctrl + Shift + F) for dispatch and change the related action syntax in those places:
  1. remove the new keyword

  2. change the action name to lowerCamelCase

  3. change the passed parameter to an object

E.g.:

this.store.dispatch(new ShoppingListActions.StartEdit(index));

... becomes ...

this.store.dispatch(ShoppingListActions.startEdit({index}));

... and ...

this.store.dispatch(new RecipesActions.AddRecipe(this.recipeForm.value));

... becomes ...

this.store.dispatch(RecipesActions.addRecipe({recipe: this.recipeForm.value}));

Important: Don't forget to apply all changes to the remaining actions in the two effect files as well, even though you won't find them via a search for dispatch there!

  • and please note:
  1. Inside the reducers I replaced Max' transformation logic by one-liners, using concat/map/filter. This is not related to the new NgRX syntax; it just makes this thread a bit shorter ;)

  2. In my code I removed the editedItem property from the shopping list state, since it's kind of redundant. It would only be used in one place of the app (shopping-edit.component.ts), and there it can be easily accessed via its index:

...
.subscribe(stateData => {
  const index = stateData.editIndex;
  if (index > -1) {
    this.editedItem = stateData.ingredients[index];
    ...
  1. In the Resolver class don't forget to change the generic type of the Resolve interface from Resolve<Recipe[]> to Resolve<{recipes: Recipe[]}>, and the return value in the else case from recipes to {recipes}.
⚠️ **GitHub.com Fallback** ⚠️