Some things I wish I knew before I start to work with Angular: NgRx
It will be final peace on my series abut lessons learned in angular. I will write other pieces but they won’t be about basic building blocks of angular.
As soon as we start to move beyond writing hello world application in angular. We start to run in to serious problem, how do we manage state in our application.
There are two problem with the state in the angular application, first it has to be expressed (written down) using objects (that becomes our data model), and it has to be shared between components.
One solution is to use @Input, @Output annotations and data binding to communicate state changes to other components. Two problems with that approach: one if application has more that 3 components the task becomes tedious and complicated (and as the title of this blog suggest I hate complexity) two you can’t pass or emit event from components that are used as router path. To that option is out the window for anything more complicated the home page that I doing for my grandpa :).
Second option is to use services. Now that one is more viable. You at least don’t have the worry about <router-outlet> if you get your scopes right you can inject service everywhere you need it. The problem is that is simple only at the beginning. As your application grows so is the number of services. Object that represent the state mutate under you(you can fix that with immutable.js) introducing hard to track bugs. And the whole thing becomes a mess that someone will want to rewrite.
There is a third option, my recommendation, and that is to use NgRx. It is very simple library that allows us to hold the state in one place and inject single service that allows us to listen to the changes. It uses the RxJS to communicate change to our state, data model so it fits the opinionated angular landscape. It is immutable so all those bugs that offend appear when dealing with complicated state disappear. If your project is going to be big I recommend to introduce NgRx right from the start. It is way easier to refactor when new businesses requirement forces developer to change his/her beautifully thought out structures :). I will not consider that over-engineering as you will see in the minute using NgRx is just using services but more tidy.
How to start
There is plenty of good tutorials, but I want to make this articles consistent. And this will be part of demonstration how simple using ngrx is. Install it:
npm install @ngrx/store
Then we need two things action and reducer. Action is an object that you dispatch when you want your application state to change. That is passed to the reducer, function that’s job is to take the action and use it’s payload to create new state. State is immutable and is persisted to the store. Where it can be retrieved from by other components. The diagram below shows this very simple flow. (There are also selectors that allow us to do some narrowing of the results, thus keeping out code cleaner. I will talk about the, leater.)
//AddToMenu.action.ts
export class AddToMenu implements Action {
readonly type:string = '[Menu] addToMenu'
constructor(payload:any){
this.payload = payload;
}
}//MenuReducer.reducer.ts
export function menuReducer(state = {appState:null}, action:Action){
if(action.type == '[Menu] addToMenu'){
return {appState: action.payload};
}
}//Menu.component.ts
export class MenuComponent implements OnInit {
constructor(private store$:Store<Action>){}
ngOnInit(){
this.store$
.subscribe(t => console.log(t.payload))
}
}
Then you have to register that reducer, but I don't want to write fully functioning example, as there are plenty of good resource out there (https://ngrx.io/guide/store). The point that I’m trying to make is that using NgRx is really easy to start with. But things get a little bit complicated :)
As we grow !!!
The big benefit of NgRx is the fact that the store is immutable. That means that everytime the reducer runs int has to return the entire new state (not for the entire application but for that pece the it is responsible). The state of our application is just json object and every reducer creates entry on the first level. So using example below to represent state in our application:
{
"image":
{
"url": "images/0001.jpg",
"width": 200,
"height": 200
},
"thumbnail":
{
"url": "images/thumbnails/0001.jpg",
"width": 32,
"height": 32
},
"batters":
{
"batter":
[
{ "id": "1001", "type": "Regular" },
{ "id": "1002", "type": "Chocolate" }
]
},
"topping":
[
{ "id": "5001", "type": "None" },
{ "id": "5002", "type": "Glazed" },
{ "id": "5005", "type": "Sugar" }
]
}
}
We can isolate 3 reducers: Image.reducer.ts, Thumbnail.reducer.ts and Batters.reducer.ts. Now if we want to add, or modify the highlighted topping then means that entire butters object has to be returned from the reducer. with that one property updated. As you can imagine your reducer can get complicated very fast with a lot nested if else statements. So my advice 1: If you have access to the entire object pass it on to your reducer (Not just changes and try to figure out what to update). It will keeps things simple. advice 2: Think about state structure constantly and refactor if necessary. As a former database developers I can tell that there is general fear when it comes to refactoring existing data model. Developers prefer to put a ton of code just to avoid messing it up. And as storage is a little like database (with only one user) I urge you to keep it as flat as possible. Better to have many reducers that are short and concise then one gigantic. The clean code evangelist preach short method for a long time and as reduces are just methods they should obey the same principles.
As we grow part 2 :)
In part two we will be talking about the other part of the equation. Reading state. In the example above I wrote:
export class MenuComponent implements OnInit {
constructor(private store$:Store<Action>){}
ngOnInit(){
this.store$
.subscribe(t => console.log(t.payload))
}
}
That console.log will be triggered every time there is any change to the store. And this is not what we want. This component should be notified only when changes that are of interest to it happened. In the diagram above there is a circle just under component that reads selectors. And this is exactly what we are going to use.
export class MenuComponent implements OnInit {
constructor(private store$:Store<Action>){}
ngOnInit(){
this.store$.pipe(select('batters'))
.subscribe(t => console.log(t.payload))
}
}
The above code will be triggered only when the change to the batters part of our model is made. Although we pass select to the pipe method it is not a function from RxJs. This operator is provided by the ‘@ngrx/store’. So advice 3: take to only things you need. ‘It's not how you store, it’s how you ask for it’.
But it is about what you store. aka: S.H.A.R.I.
To be honest this paragraph will be a ripoff of this talk. But as picasso said: “Good artists borrow great artists steal.” :). Plus I’ve been using the S.H.A.R.I. principle and I found it complete. Let’s face it NgRx is complicated (it is as complicated as it has to be to preserve clarity and verbosity) so it doesn’t make sense to put everything in the store. This are some principles to guide us thru the decision process.
Shared — Shared state meaning that is not consumed by one element. I am a big advocate of separating business and display logic in to smart and presentation components. And I prefer to have one to one relationship. One presentation component wrapped around one smart component. And they have they own way of sharing data. So when I used term element I ment a grouping of the two. Great example of shared state is authorization data. It will be needed everywhere.
Hydrated — as it has to be available next time you have to boot up. So this is all the cases where sessionstorage or localstorage come to play. User preferences are a good example of this.
Available — I mentioned before that state that is access only form one component should not be persisted in the storage, but there is an exception. If the data to render this one component come from the route. Especially if you have to hit backend for that information.
Retrieved — I touched on this before. If you call the external API to retrieve some data, that data becomes good candidate for storage. To be honest this one is a little bit of a judgment call. There are other ways to cache and re-emit data that have been fetched using http.get(‘htps://…’). For example using publishReplay(1), refCount(). There is a cool mechanizm in NgRx ecosystem that allows to move those side effects to a different peace of code. Thus keeping our components pure, called @ngrx/effects but I will not talk about here.
Impact — state that is impacted by actions from other stores. To be honest i think that this one is a little bit redundant (folds nicely with the shared state) and i think the outhores just wanted to spell that mnemonic device a certain way. Just like S.H.I.L.D.
So now to the world of my personal opinions. What we should not put in the store. Number one forms. This is a little bit controversial. For example this talk gives a different view point. But I feel like the Angular reactive forms API (that i recommend using) is it’s own from of storage. However forms are full of state full information, and we know that these are good candidate for persistence. I hear that argument but wat ultimately convinces me it that the context is usually self contain it is not shared anywhere. Also angular controls that make the form are nighter serializable or immutable, they will change without your knowing. Advice 4: Think what to put in a store.
Naming convention and some useful patterns.
This paragraph is going to be a link. Victor Savkin is one of the smartest guys in the angular community. In this article he gives a great advice on how some of the most popular messaging patterns translate into NgRX. He takes the examples from the book co-authored by Martin Fowler, and translate the mechanisms that can be found in big and small data centers across organizations into to localized one user pattern. And I can’t really improve on that.
NgRx is not a silver bullet. Summary.
Fred Brooks in 1989 wrote and article ‘No silver bullet — essence and accident in software engineering’. There he explores the reasons why developers productivity can’t scale with the sames speed as moore’s law describes scaling hardware. It boils down to that we are dealing with real world problems that are as complicated as the are. And we can’t make them any simpler. I mean creating a bank account takes 50 steps (required by the regulators, parent company etc) and we will never be able to reduce that number. They are the essence of the business problem that we are trying to solve. We we aptly call it essential complexity. There is also accidental complexity this one it totally reducible as it is a fault of developer. This blog is my way of fighting that particular form of stupidity.
Anyway as i started to drift away from the subject. I believe the NgRx when used properly (or at least consistently) can help us reduce accidental complexity. By helping us avoid service injection hell and mutation hell that will inevitably happen if we try to solve them by yourself.