NgRx Course – NgRx Store Devtools and Action Types

In the last video, we installed the NgRx libraries, set it up and created the basic structure for our application, including an action and a reducer.

In this video, we’ll have a look at NgRx store-devtools and we’ll continue building the application and introducing some best practices, like strongly typed actions.

We already installed the store-devtools package in the previous video, so all we need to do is import it in the app.module.ts file:

// app.module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

// NgRx
import { StoreModule } from "@ngrx/store";

import { AppComponent } from "./app.component";
import { HomeComponent } from "./home/home.component";
import { NavbarComponent } from "./navbar/navbar.component";
import { AppRoutingModule } from "./app-routing.module";

import { StoreDevtoolsModule } from "@ngrx/store-devtools";

@NgModule({
  declarations: [AppComponent, HomeComponent, NavbarComponent],
  imports: [
    BrowserModule,
    StoreModule.forRoot({}),
    StoreDevtoolsModule.instrument(),
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})

export class AppModule {}

Next, we need to add the Redux Devtools Extension to our browser (http://extension.remotedev.io/). Once we’ve done that, we’ll be able to inspect the store in the browser.

In the Redux panel, we can track all the actions and changes in the store. We can see the changes in the state, the whole state tree and the actions being dispatched. We also have different options for the view. Such as the raw object and the chart and we have the possibility to do time travel debugging.

Now that we have that ready, let’s continue with our application. We’re going to define strongly typed actions and the reducer needed to load the customers. We’ll also build the service to communicate with the API.

Let’s start with the actions, so open the customer.actions.ts file and start declaring our actions there:

// customers/state/customer.actions.ts
import { Action } from "@ngrx/store";

// Import the customer model
import { Customer } from "../customer.model";

export enum CustomerActionTypes {
  LOAD_CUSTOMERS = "[Customer] Load Customers",
  LOAD_CUSTOMERS_SUCCESS = "[Customer] Load Customers Success",
  LOAD_CUSTOMERS_FAIL = "[Customer] Load Customers Fail"
}

export class LoadCustomers implements Action {
  readonly type = CustomerActionTypes.LOAD_CUSTOMERS;
}
export class LoadCustomersSuccess implements Action {
  readonly type = CustomerActionTypes.LOAD_CUSTOMERS_SUCCESS;

  constructor(public payload: { customers: Customer[] }) {}
}
export class LoadCustomersFail implements Action {
  readonly type = CustomerActionTypes.LOAD_CUSTOMERS_FAIL;

  constructor(public payload: string) {}
}

export type Action = LoadCustomers | LoadCustomersSuccess | LoadCustomersFail;

Firstly, import the Actions interface from the store to define the actions and prevent mistakes. Then import the Customer model. We then need to list the valid action types using enum, a typescript type that allows us to define a set of constants.

We then create the actions with the action creators. These are classes with two properties, types, that we specify as readonly and the optional payload, defined in the constructor. This will allow us to use strongly typed actions in our application.

Finally, we define a type that combines all of the actions. We then export it to make it available to the rest of the application.

Now we can take advantage of strongly typed actions in our app.

Let’s go ahead and create the reducer. But before we do that, we’re going to define an interface for the state of the application. We’re going to create a folder called state inside the app/ folder. We’re going to add a file with the name app-state.ts were we’ll define the state tree. The next code is only an example, we’re not going to define the state this way:

// app-state.ts
import { AppState } from "./app.reducer";
import { CustomerState } from "../customers/state/customer.reducer";

// Define the state tree with it's feature slices
export interface AppState {
   app: AppState,
  customers: CustomerState
}

That’s how we would do it in a larger application, defining the slices of state inside the AppState interface. However, for our small sample application, we are not going to include the slices of state. This is because we don’t want to break the lazy loading boundaries and we don’t have a root app state. So we’ll leave the app-state.ts file like the following:

// app-state.ts
export interface AppState {}

Now we’re going to build our reducer. First. we import the actions, the customer model and the app state interface:

// customers/state/customer.reducer.ts
import * as customerActions from "./customer.actions";
import { Customer } from "../customer.model";
// import the app state
import * as fromRoot from "../../state/app-state";
…
// (to be continued)

Then we export the customer state interface and the app state interface. We also define the initial state of the application with default values:

// customers/state/customer.reducer.ts (continued)
...
// export the customer state interface
export interface CustomerState {
  customers: Customer[];
  loading: boolean;
  loaded: boolean;
  error: string;
}

// export the app state
export interface AppState extends fromRoot.AppState {
  customers: CustomerState;
}

// define the initial state of the applciation with default values
export const initialState: CustomerState = {
  customers: [],
  loading: false,
  loaded: false,
  error: ""
};
...
// (to be continued)

Then we declare and export the reducer function:

// customers/state/customer.reducer.ts (continued)
...
export function customerReducer(
  state = initialState, 
  action: customerActions.Action): CustomerState {

  switch (action.type) {

    case customerActions.CustomerActionTypes.LOAD_CUSTOMERS: {
      return {
        ...state,
        loading: true
      };
    }

    case customerActions.CustomerActionTypes.LOAD_CUSTOMERS_SUCCESS: {
      return {
        ...state,
        loading: false,
        loaded: true,
        customers: action.payload
      };
    }

    case customerActions.CustomerActionTypes.LOAD_CUSTOMERS_FAIL: {
      return {
        ...state,
        customers: [],
        loading: false,
        loaded: false,
        error: action.payload
      };
    }
    
    default: {
      return state;
    }
  }
}

In the reducer, we’re passing the initial state of the application as the first argument and specifying the action type for the second one. The reducer is a switch statement that will return modifications of the state depending on the action being dispatched. As we can see, we’re using the strongly typed actions that we created before. This is so we can take advantage of the tooling that typescript provides. It also minimizes the possibility of making mistakes.

With that in place, let’s go ahead and create our first Angular effect to fetch the data from the server, add it to the store and display it in the component. We’ll do that in the next video.