15. Strongly type our store

In this section we will add strong typing using TypeScript to our applications state logic. We skipped this step to make it simple to see what redux is and how the pattern works.

1. Add types to our reducer

By community convention most people put the types for each slice of state with the reducer, which makes sense as this is where you update the slice of state. Here will will make an interface for shape of the slice of state and use it to type a constant to make our initial state. It is a good idea to always make initial state so we can be confident what we will get when we start subscribing to the store before any actions that might update the state are dispatched.

  • Add an interface and initial state to our spinner reducer.

src/app/state/spinner.reducer.ts
export interface State {
  isOn: boolean;
}

export const initialState: State = {
  isOn: false
};

export function reducer(state = initialState, action): State {
  switch (action.type) {
    case 'startSpinner': {
      return {
        isOn: true
      };
    }

    case 'stopSpinner': {
      return {
        isOn: false
      };
    }

    default:
      return state;
  }
}

2. Add a global state interface

This is a difficult task as most applications lazily load the majority of their state, meaning we can only specify the state that is always loaded when the app loads. We can then extend our feature state objects to have these "global" state reducers.

  • Create a state.ts file in our state folder.

  • Add a global interface to describe all the global state that will be initialised up front in our application on load.

src/app/state/state.ts
import * as fromSpinner from './spinner/spinner.reducer';

export interface State {
  spinner: fromSpinner.State;
}

3. Use global interface in EventComponent

Now we can inject the Store into our components with a type. This is useful to say what state reducers we should be able to interact with from this component but in reality the store is a global object and these types do not stop us getting the whole store.

  • Update the store to use the new State interface.

src/app/event/container/event/event.component.ts
----------- ABBREVIATED CODE SNIPPPET ----------
  
  constructor(
    private store: Store<State>,
    private eventService: EventService
  ) {}


----------- ABBREVIATED CODE SNIPPPET ----------

4. Create action creators

Action creators are a big part of making an Angular application that uses NgRx more robust. It helps get type inference and code hints in our app and stops users dispatching actions with mistakes or the wrong actions with maybe the wrong payloads.

The SpinnerActionTypes enum is useful for having a strongly typed list of the actions we can dispatch. It is also handy to have detail in them so when we see them in our dev tools we can also see the place and time they are dispatched from, making debugging easier when we are less familiar with the code written by other or even ourselves.

  • Create a spinner.actions.ts file in our state/spinner folder.

  • Make action creators to strongly type our actions.

src/app/state/spinner/spinner.actions.ts
import { Action } from '@ngrx/store';

export enum SpinnerActionTypes {
  StartSpinner = '[Spinner Page] Start Spinner',
  StopSpinner = '[Spinner Page] Stop Spinner'
}

export class StartSpinner implements Action {
  readonly type = SpinnerActionTypes.StartSpinner;
}

export class StopSpinner implements Action {
  readonly type = SpinnerActionTypes.StopSpinner;
}

export type SpinnerActions = StopSpinner | StartSpinner;

5. Use action creators in our components

  • Use action creators in EventComponent and remove our un typed actions we are dispatching.

src/app/event/event.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';

import { Attendee } from '../../../models';
import { EventService } from '../../services/event.service';
import { State } from '../../../state/state';
import { StartSpinner, StopSpinner } from '../../../state/spinner/spinner.actions';

@Component({
  selector: 'app-event',
  templateUrl: './event.component.html',
  styleUrls: ['./event.component.scss']
})
export class EventComponent implements OnInit {
  spinner$: Observable<boolean>;
  attendees$: Observable<Attendee[]>;

  constructor(
    private store: Store<State>,
    private eventService: EventService
  ) {}

  ngOnInit() {
    this.getAttendees();
    this.spinner$ = this.store.pipe(select(state => state.spinner.isOn));
  }

  getAttendees() {
    this.attendees$ = this.eventService.getAttendees();
  }

  addAttendee(attendee: Attendee) {
    this.store.dispatch(new StartSpinner());
    this.eventService.addAttendee(attendee).subscribe(() => {
      this.store.dispatch(new StopSpinner());
      this.getAttendees();
    });
  }
}

6. Use ActionTypes and Actions type in reducer

We will break our unit tests by using our SpinnerActionTypes which now have a different string and words for the type argument of our actions but we will fix these in the next section.

  • Type the action argument passed into our reducer function as SpinnerActions.

  • Change our case statements to use our new SpinnerActionTypes.

src/app/state/spinner.reducer.ts
import { SpinnerActionTypes, SpinnerActions } from './spinner.actions';

export interface State {
  isOn: boolean;
}

export const initialState: State = {
  isOn: false
};

export function reducer(state = initialState, action: SpinnerActions): State {
  switch (action.type) {
    case SpinnerActionTypes.StartSpinner: {
      return {
        isOn: true
      };
    }

    case SpinnerActionTypes.StopSpinner: {
      return {
        isOn: false
      };
    }

    default:
      return state;
  }
}

Extras and Homework

These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises.

WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course.

You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you.

Learn more about Action Hygiene from the NgRx team in this ngConf you tube video

Steps:

  1. Watch the video

Last updated