NGRX - from the beginning, part III, NGRX Store

Chris Noring - Apr 6 '19 - - Dev Community

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

NGRX is an implementation of the pattern Redux. It's made for the framework Angular and adds Typescript and reactiveness with RxJS

This article is part of a series:

  • NGRX — from the beginning, part I, Pub-sub,
  • NGRX — from the beginning, part II, part II Redux
  • NGRX — from the beginning, part III, NGRX store, we are here
  • NGRX — from the beginning, part IV, NGRX store, improving our code
  • NGRX — from the beginning, part V, NGRX effects, in progress
  • NGRX — from the beginning, part VI, NGRX entity, in progress

If you come into this part with no idea what Redux or Pub-Sub is, I urge you to read part I and part II first. We won't explain things like Reducer or Action or how we dispatch messages cause that would make this article too long.

In this article, we will cover

  • Set up and install, we will explain how to scaffold a project and also install the necessary dependencies and we will also add our first state property and Reducer function
  • Displaying data, here we will cover how to inject the Store service and how to display data from the Store in our markup
  • Change state, after we've shown how to show the data we will here focus on how to change the data or as it is referred to with Redux, to dispatch an action

Set up and install

Ok, we need to do the following:

  1. Scaffolding an Angular project
  2. Install @ngrx/store
  3. Set up @ngrx/store

Install

Ok, first things first, let's create ourselves an Angular project by typing the following in the terminal:

ng new NameOfMyProject
cd NameOfMyProject

Thereafter we remain in the terminal and enter:

npm install @ngrx/store

This install our library and we are ready to configure our project to @ngrx/store.

Set up

Ok, we have a project let's set it up. There are two things we need to do:

  1. Import the StoreModule
  2. call StoreModule.forRoot() and provide it with an object consisting of a number of state, reducer pairs

Ok, we open up app.module.ts and enter:

import { StoreModule } from '@ngrx/store';

Lets call StoreModule.forRoot() next. As mentioned above we need to give it an object consists of a number of state, reducer pairs, like so:

{
  state: stateReducer, 
  secondState: secondStateReducer
  ...
}

Ok, we understand the overall shape so let's show in real code what this might look like:

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default:
      return state;
  }
}

Next, in the same file app.module.ts, we need to add our StoreModule to our list of imports, like so:

imports: [
  BrowserModule,
  AppRoutingModule,
  StoreModule.forRoot({
    counter: counterReducer
  })
]

As you can see we are now providing an object to StoreModule.forRoot() containing our counter, counterReducer pair.

Displaying data

Ok, that covers the bare minimum set up we need to make @ngrx/store work. However we can't show our data at this point. To show data we need to inject the Store service into a constructor of the component that aim to display data. Let's go into app.component.ts and add the needed import:

import { Store } from '@ngrx/store';

And the needed injection into the component class constructor:

constructor(private store: Store<any>) {}

At this point you might wonder about the use of any as template argument to the Store, let's leave that for now and work on improving it in just a little bit.

 Selecting data

Ok, the Store service will let us select data from its inner state if we use the method pipe() and the operator select(), like so:

import { Store, select } from '@ngrx/store';

@Component({
  ...
})
export class AppComponent {
  counter$;

  constructor(private store: Store<any>) {
    this.counter$ = this.store.pipe(
      select('counter')
    )
  }
}

We are importing the select() operator from @ngrx/store and we also assign the result of our pipe() invocation to a variable counter$. counter$ is an Observable and that means we can use an async pipe to show the content. Let's now head to our app.component.html file and type the following:

My counter: {{ counter$ | async }}

Save an run your project and your browser should now display My counter: 0

 With a function selector

So far everything works, but let's go back to how we inject our Store service in the AppComponent constructor. We gave it the template argument any. We can definitely improve that, but how? The idea is to feed it an interface that represents your Store state. Currently, your Stores state looks like this:

{
  counter
}

So let's create an interface that corresponds to this, namely:

// app-state.ts
export interface AppState {
  counter: number;
}

Above we are declaring the only existing state property counter and we declare what type it is as well. Ok, next step is to use AppState for our Stores template, so let's update app.component.ts.
First add this import:

import { AppState } from './app-state';

Let's now replace any with AppState:

constructor(private store: Store<AppState>){}

We can do more though, we can leverage this by switching from the select() method that takes a string to the one that takes a function, like so:

So this:

this.counter$ = this.store.pipe(
    select('counter')
  )

Becomes:

  this.counter$ = this.store.pipe(
    select(state => state.counter)
  )

What's so great about this you ask? Well now we get help from the compiler if we attempt to select a property from the state doesn't exist, only state.counter would be a valid choice. Also if our state is nested this is the only way we would be able to reach a state like state.products.data for example, if we had a state products that is.

 Change state

Ok, great, we can display a state property from our store, what about changing it? For that, we have a dispatch() method that will allow us to send a message to the Store. The message is in the form of an Action object. Ok, let's build out our UI a bit. Go into app.component.html and add the following:

// app.component.html

<button (click)="increment()">Increment</button>

Now, head to app.component.ts and add the method increment(), like so:

@Component({})
export class AppComponent {
  increment() {
    // add dispatch call here
  }
}

Ok, we need to do the following to send a message to the store:

  • Construct an action object with the message type INCREMENT
  • Call dispatch() on the Store service instance

Ok, to construct an object we can use an object literal, like so:

const action = { type: 'INCREMENT' };

The reason for using the message INCREMENT lies in how we constructed the counterReducer function, let's look at at it quickly:

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    default:
      return state;
  }
}

It's clear it expects a message that has a property type and for that message type value to be INCREMENT. Ok, looks good, lets make the call to dispatch():

@Component({})
export class AppComponent {
  // constructor omitted for brevity
  increment() {
    const action = { type: 'INCREMENT' };
    this.store.dispatch(action);
  }
}

Invoking increment() will now lead to our message being sent to the store and for the counterReducer() to be invoked and for our counter value to be updated by one. Try it yourself.

Summary

That's all we planned to cover in this part. We've shown how to install @ngrx/store but also how to set up a state, reducer pair in our StoreModule.forRoot(). Furthermore, we've shown how you can select data from the Store service and also how to update data by using the mentioned Store service and its dispatch() method. In the next part we will look at a more rich example and look at how we can improve things using enums and built in interfaces, so stay tuned :)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .