State management in Angular with ngrx
Gabriel Katz
Content
- Some Prerequisites
- ngrx store and how to make it work
- Advanced ngrx topics
- Pros & Cons
Prerequisites - RXJS
Primitives
- Observable: stream of events
- Subject: Observable of which input can be controlled
- BehaviorSubject: Subject that gives the subscribers its current state first
Prerequisites - Important RXJS Operators
map: Apply function to each emitted value.
filter: Only return emitted values that fulfill predicate.
scan: Like a reduce, but emits intermediate result.
Prerequisites - Important RXJS Operators
mergeMap: Create observable for each emitted value, and merge observables
Prerequisites - Important RXJS Operators
switchMap: Only listen to observable created from most current emitted value
Prerequisites - State in angular Applications
What is ngrx?
- A state container for angular
- Implementation of the flux pattern
- Similar to redux, but implemented using rxjs
Issue with "Javascript MVC"
From https://brigade.engineering/what-is-the-flux-application-architecture-b57ebca85b9e
Flux pattern
From https://brigade.engineering/what-is-the-flux-application-architecture-b57ebca85b9e
Bring the complexity!
From https://brigade.engineering/what-is-the-flux-application-architecture-b57ebca85b9e
What the store (more or less) does
export type Reducer<S, A> = (state: S, action: A) => S;
@Injectable()
export class Store<S, A> extends BehaviorSubject<S>{
private actions = new Subject<A>();
constructor(private reducer: Reducer<S, A>, public initialState: S) {
super(initialState);
this.actions.scan((state, action) => this.reducer(state, action), initialState)
.subscribe(super);
}
dispatch(action: A): void {
this.actions.next(action);
}
}
Parts you need to write to make ngrx work:
- Actions
- Reducer: (State, Action) => State'
- Registration
- Views
A very simple ngrx app - actions
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
export class IncrementCounterAction implements Action{
readonly type = INCREMENT;
constructor(public payload: number = 1){
}
}
actions/counter.ts
A very simple ngrx app - reducer
export function counterReducer(state: number = 0, action: Action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
case RESET:
return 0;
default:
return state;
}
}
reducers/counter.ts
Testing a reducer is easy!
A very simple ngrx app - registration
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot(mainReducer),
// pre-4.x
StoreModule.provideStore(mainReducer)
]
})
export class AppModule {}
interface AppState {
counter: number;
}
export const mainReducer = { counter: counterReducer };
reducers/index.ts
app.module.ts
A very simple ngrx app - View
@Component({
selector: 'my-app',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="increment()">Increment</button>
<div>Current Count: {{ counter$ | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
`
})
class MyAppComponent {
counter$: Observable<number>;
constructor(private store: Store<AppState>){
this.counter$ = store.select((s: AppState) => s.counter);
}
increment(){
this.store.dispatch(new IncrementCounterAction());
}
...
}
A slightly improved app - selector
class MyAppComponent {
constructor(private store: Store<AppState>){
this.counter$ = store.select(getCounter);
}
}
export const getCounter = (state: AppState) => state.counter;
reducers/index.ts
Summary: We have
- written Actions and dispatched them
- set the initial state and updated it using a reducer.
Q: How does this approach scale?
combineReducers
interface PartState {
foo: A;
bar: B;
}
export partReducer = combineReducers({ foo: fooReducer, bar: barReducers })
File structure: Classic naming convention
- reducers/index.ts: combined objects and reducers
- actions/{storagePart}.ts: action
- reducers/{storagePart}.ts: reducer and selectors
Alternative file structure
- reducers/index.ts: combined objects / registration
- /{feature}/actions/{actionName}.ts: action
- /{feature}/reducers/{reducerName}.ts: reducer and selectors
Advanced topics
- effects
- routing
- db
- dev-tools
- auxiliary libraries
Helsana Clearview
- Order accident and illness insurance for companies
- Questionnaire: one large data structure exchanged with server
- Saved and then store at each page navigate
Demo
Uses for effects
- Asynchronous actions (workers, rest calls, etc.)
- Make state from one part of store available for other part
Effects
@Injectable()
export class CollectionEffects {
@Effect()
addBookToCollection$: Observable<Action> = this.actions$
.ofType(collection.ADD_BOOK)
.map((action: collection.AddBookAction) => action.payload)
.mergeMap(book =>
this.db.insert('books', [ book ])
.map(() => new collection.AddBookSuccessAction(book))
.catch(() => of(new collection.AddBookFailAction(book)))
);
constructor(private actions$: Actions, private db: Database) { }
}
@NgModule({
imports: [
BrowserModule,
StoreModule.provideStore(...),
EffectsModule.forRoot([CollectionEffects]),
// pre-4.x.x
EffectsModule.run(CollectionEffects)
]
})
export class AppModule {}
Testing Effects
describe('collection effects', () => {
let collectionEffects: CollectionEffects;
let effectsRunner: EffectsRunner;
beforeEach(() => TestBed.configureTestingModule({
imports: [
EffectsTestingModule
],
providers: [
CollectionEffects,
...
]
}));
beforeEach(() => { /* assign variables from TestBed.get(...) */});
it('should load collection', () => {
...
effectsRunner.queue(new AddBookAction());
collectionEffects.addBookToCollection$.subscribe(result => {
expect(result).toEqual(expectedResult);
});
});
});
Router & Store
Goal: Synchronize router state and store state
Old implementation:
Store
Router
dispatch( go(['foo']))
navigate(['foo'])
UPDATE_LOCATION()
4.x implementation:
Router
Store
dispatch(
ROUTER_NAVIGATION)
navigate(['foo'])
Action will be cancellable by throwing exception!
DB
- Saves ngrx state to indexedDb
- should come with the ngrx 4.0 platform!
Store-Devtools
- Inspect last few store states
=> DEMO!
3rd party libraries to combine with rxjs
- ngrx-store-freeze: Enforce immutability for development
- immutable.js: Immutable types for store
- reselect: Combine selectors, only update on change
- ramda.js: Functional library for immutable objects
Problems that ngrx solves
- A separate layer for app state
- Component interaction with observables
- Client-Side Cache
- Put temporary UI state before saving to server
- Allow modification of data by multiple actors
When ngrx store does not help much
- you are new to angular
- app is very simple
- data is not shared between views
Questions & Discussion
Useful links:
- https://github.com/ngrx/platform
-
https://blog.nrwl.io/managing-state-in-angular-applications-22b75ef5625f (Victor Savkin)
new version!
https://blog.nrwl.io/using-ngrx-4-to-manage-state-in-angular-applications-64e7a1f84b7b - https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367
- http://blog.angular-university.io/angular-2-redux-ngrx-rxjs/
- https://gist.github.com/btroncone/a6e4347326749f938510
Thank you:
Emil Effila
Nino Lanfranchi
André Fröhlich
everyone else at ti&m
everyone at Triarc Laboratories
Dibran Isufi
Marcel Tinner
State management in Angular
By gabrielkatz
State management in Angular
- 1,203