Michal Majewski

@MichalMajewski

What I have learned using ngrx/Redux with Angular 2

Most of Angular developers including me have some back-end programming background. It has some advantages but it has also some implications on fact, how we solve certain problems. We see world as bunch of objects which are containers for responsibilities. In redux you have to switch your mindset a bit.

Do not fight with boilerplate.

When I started with redux I hated these action creators and this big and ugly switch statement. So I started with defining some conventions inspired by alt.js implementation. Then I have implemented something like below

export class SavedConfigurationStore {

setBusy(state, _, busy) {
return {...state, busy);
}

invalidate(state) {
return {...state, invalidate: true};
}

onFetchSavedConfigsSuccess(state, action) {
return {...state, configs: action.payload, invalidate: false};
}
  ....
}
const savedStore = new SavedConfigurationStore();
export const savedConfigurationReducer = ReducerBinder.start()
.bindActionsByConvention(SavedActions, savedStore)
.bindAsyncAction(SavedActions.FETCH_SAVED_CONFIGS, savedStore.setBusy)

I created that just to have nice simple POJO objects with actions. Then i defined this ReducerBinder class to bind my object methods as action handlers. However it make look more appealing for back-end developer, it has a lot of downsides. Nobody except me know this convention, if I will force my team mates to build whole application based on this redux wrapper they quickly gonna hate me. Secondly it uses OO patterns. Redux is about using functional approach, to have everything divided into small composable functions. In above example we have everything in one big object.

This boilerplate which redux is coming with has more advantages than downsides. You have full control. You will be truly surprised, how easy with that architecture it will be, to implement offline state, redo/undo functionality, optimistic update functionality and many more. There is no reason to fight with that. If you wanna have full control over your code you don’t wanna cover everything with abstraction.

Normalize your state

You should keep your reducers relatively small and compact. When your state is not normalized and you are keeping everything inside one big model, it will end up with big mess. In your reducer you will have whole bunch of tools from lodash just to traverse these big tree of objects and find one you wanna update. When you finally find leaf object, you wanna update you need to create new object for each parent of that object up to the root of your model.

const initialState = {
genres: [
{
id: 1, name: 'rock', bands: [
{
id: 1, name: 'led zeppelin', albums: [
{id: 1, name: 'I', items: 3},
{id: 2, name: 'II', items: 66},
{id: 3, name: 'III', items: 3},
]
},
{id: 2, name: 'boston'},
]
}
]
};


export const albums = (state = initialState, {type, payload}) => {
switch (type) {
case UPDATE_ITEMS_COUNT:
const
nextGenres = state.genres.map(g => {
const nextBands = g.bands.map(b => {
return b.albums.map(a => {
if (a.id != payload.id) {
return a;
}
return {...a, items: payload.items};
});
});
return {...g, bands: nextBands}
});

return {...state, genres: nextGenres};
default:
return
state
}
};

That’s how your code will be looking like. When you will normalize your sate you will have three reducers: genres, bands, albums. Your responsibility will be distributed properly. Albums reducer will be responsible for performing updates on album models. Genres and bands reducers can be responsible for moving albums around from one group to another.

const initialState = {
[1]: {id: 1, name: 'I', items: 3},
[2]: {id: 2, name: 'II', items: 66},
[3]: {id: 3, name: 'III', items: 3}
};


export const albums = (state = initialState, {type, payload}) => {
switch (type) {
case UPDATE_ITEMS_COUNT:
const
{id, items} = payload;
const album = state[id];
return {...state, [id]: {...album, items}};
default:
return
state
}
};

Above you can look how normalized reducer looks like. It’s worth to notice that you not only have flat structure but you only keep all object on “dictionary” instead of keeping everything on array. When you have dictionary with keys as ids it’s much easier to find object for update.

How can i normalize state?

It can be done manualy inside your service class implementation. Or you can use third party libraries that helps with that. I higly recommend to take a look on normalizr library. With normlizr you are defining schema data retrieved from server liek below:

import { normalize, schema } from 'normalizr';

const album = new schema.Entity('album');
const bands = new schema.Entity('bands', {
albums: [album]
});
const genres = new schema.Entity('genres', {
bands: [ bands]
});

const normalizedData = normalize(originalData, genres);

and result will be like below

var a = {
result: "123",
entities: {
"genres": {
"123": {id: 1, name: 'rock', bands: [1, 2]}
},
"albums": {
"1": {id: 1, name: 'I', items: 3},
"2": {id: 2, name: 'I', items: 3},
"3": {id: 3, name: 'I', items: 3}
},
"bnads": {
"1": {id: "1", name: 'led zeppelin', albums:["1", "2", "3"]},
"2": {id: "1", name: 'boston', albums: []},
}
}
};

How to denormalize state for views?

However normalization is simplifying updates operations, it makes reads bit more complex. But it also gives you more flexibility. When one of your view needs join from two entities and other view needs join from 3 other entities, then you can do this relatively easy. It also have good impact on your performance. When your view needs as view model only 2 entities and you are updating 3rd one, then your view is not being refreshed unnecessary. But you have to use onPush notification mode and you have to denormalize data for view model smartly.

I was looking for some advices for that and didn’t find anything helpful so i made my way of denormalization sate. If someone has better approach for that please let me know.

Let’s say we wanna have view that presents our data in exact same shape as initial model was:

<div class="genre"  *ngFor="let genre of genres">
<div class="band" *ngFor="let band of genre.bands">
<div class="album" *ngFor="let album of band.albums">{{genre.name}}{{band.name}}:{{album.name}}</div>
<img [attr.src]="album.coverImage" />
</div>
</div>

Right now we cannot do this because our model is normalized. For example genre model has only list of bands ids. We can achieve that by defining some mapping selectors like below:

const getGenres = state=>{
const {albums, genres, bands} = state.entities;
return values(genres).map(g=>{
const bands = g.bands.map(bid=> {
const band = bands[bid];
const bandAlbums = band.albums.map(aid=>{
const a = albums[aid];
return {...a, coverImage : `images/${a.name}.jpg`}
});
return {...band, albums: bandAlbums};
})
})
};

store.select(getGenres).subscribe((g)=>{
this.genres = g;
})

But i wouldnt recommend you doing that becasue of two reasons:

  • we are mixing here two concerns — traversing object tree and mapping to view model.
  • every time one single album will be updated, angular will recreate whole tree of view models as new instances which will cause re-render for whole thing, even if we are using onPush strategy.

To bust performance and divide responsiblity we can do something like below:

@Component({
template: `
<div class="genre" *ngFor="let genre of (genres$|async)">
<genre-view [genre]="gendre"></genre-view>
</div>`
})
export class HomeComponent {
genres$;

constructor(store: Store<AppState>) {
this.genres$ = this.store.select(getAllGenres);
}
}

then inside GenreComponent we have code like below

@Component({
selector: 'genre-view',
template: `
<div class="band" *ngFor="let band of (bands$|async$)">
<band-view [band]="band" [genre]="genre"></band-view>
</div>
`
})
export class GenreComponent {
@Input() genre;
bands$;
constructor(store: Store<AppState>) {
this.bands$ = this.store.select(getBandsByIds(this.genre.bands))
}
}

and then inside BandComponent

@Component({
selector: 'band-view',
template: `
<div class="album" *ngFor="let album of (albums$|async)">{{genre.name}}{{band.name}}:{{album.name}}
<img [attr.src]="album.coverImage" />
</div>
`
})
export class BandComponent {
@Input() genre;
@Input() band;
albums$;
constructor(store: Store<AppState>) {
this.albums$ = this.store.select(getAlbumsByIds(this.band.ablums)).map(a=>({
name: a.name,
coverImage: `${a.name}.jpg`
}))
}
}

So far so good. But there are two problems with that solution. We are not fully getting benefit from OnPush change detection strategy. Second thing is that, we are defining list of children only once inside constructor, which means when input parameter will change we are not reacting at all. When BandComponent band input will change, it will be still rendering albums which belongs to previous band. To fix that my approach is like below.

@Component({
selector: 'band-view',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="album" *ngFor="let album of props.albums">{{album.name}}
<img [attr.src]="album.coverImage" />
</div>
`
})
export class BandComponent {
@Input() band;
band$ = new Subject<any>();
props ={};
constructor(store: Store<AppState>, private cd : ChangeDetectorRef) {
this.store.let(queryBandAlbums(this.band$)).map(a=>({
name: a.name,
coverImage: `${a.name}.jpg`
})).subscribe(a=>{
this.props = {albums: a};
cd.markForCheck();
});

}
onChanges(){
this.band$.next(this.band);
}
}


export const queryBandAlbums = band$ => store$ => {
return band$.switchMap(b => store$.select(getAlbumsByIds(b.ablums)))
.distinctUntilChanged();
};
  • We are using Angular onChanges life cycle hook to monitor every change of input parameter. Every time band parameter will change, we will get new list of albums
  • We are using OnPush change detection strategy to gain better performance. However with this strategy angular will know nothing if album object will change itself. Because of that we need to inform angular with cd.markForCheck() about changes.

With that approach we have best from two worlds. We have nice normalized data, which is easy to deal with. We have good performance because when something will change, inside only one album, then only this element will be re-rendered. All other references will remain the same. Let me know if you have better approach how to deal with normalized state with Angular.

More by Michal Majewski

Topics of interest

More Related Stories