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:
- Scaffolding an Angular project
- Install @ngrx/store
- 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:
- Import the
StoreModule
- 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 :)