visit
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
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 library. With normlizr you are defining schema data retrieved from server liek below: import { normalize, schema } from 'normalizr';
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:
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:
@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();};