NgRX: In Depth
Yauheni Pozdnyakov

RxJS
Reactive Pogramming
- An event - 1 array element
- We consider the world as pipe
- We process, filter, map, reduce data as a regular array
- Any operation is asynchronous
| 1 | args | |
|---|---|---|
| sync | value | array |
| async | promise | stream/pipe |
Async vs Sync
RxJS in details

- Observable— an object or a function that produces a sequence of data in time
- Observer — an object or a function that knows how to consume and process data
- Subscriber— an object or a function that connects Observable and Observer.
Just messaging or a pub-sub?
Rx
Producer
Consumer
Data pipeline
Time
Declarative
Immutable
Pure, side effect -free
Map, reduce, iterator
Functional Programming
RxJS in details
RxJS memory leaks?
Some examples
Array as a source
source - an array or an object
from - creates an Observable-a sequence of an array-like or iterable objects
of - creates Observable-a sequence of passed arguments
const task_stream =
// Стрим из всех данных в базе
getTasks().
// выбираем задания только для текущего пользователя
filter((task) => task.user_id == user_id).
// находим те, которые еще не завершились
filter((task) => !task.completed).
// берем только название
map((task) => task.name)Observable.from(source)
.operatorOne()
.operatorTwo()
.oneMoreOperator()
.aaanddMoreOperator()
.filter(value => value)
.subscribe(console.log);Events as a source
Observer
- next() - here is a new value in a sequence;
- error() - here is an error occurred;
- complete() - sequence is complete.

Leaks are easy
Typeahead
return Observable.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));Is everything okay?
closed = false
Memory leak!
return Observable.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));private subscription: Subscription;
onInit(){
this.subscription = this.listenAndSuggest();
}
listenAndSuggest(){
return Observable
.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
}
Forgot to unsubscribe from a Cold Pipe
Getting into Observer
Forgot to unsubscribe from a Cold Pipe
No one will Unsubscribe for you
interface Observer<T> {
closed?: boolean;
next: (value: T) => void;
error: (err: any) => void;
complete: () => void;
}
unsubscribe(): void {
let hasErrors = false;
let errors: any[];
if (this.closed) {
return;
}
let { _parent, _ parents, _unsubscribe, _subscriptions } = (<any> this);
this.closed = true;
this._parent = null;
this._parents = null;
this._subscriptions = null;
}Solution 1... Is it good enough?
We forgot to unsubscribe from a Cold Pipe
We want to consume a stream only while component is alive
Will be useless in case of many Observable

private subscription: Subscription;
onInit(){
this.subscription = this.listenAndSuggest();
}
listenAndSuggest(){
return Observable
.fromEvent(this.elemnt.nativeElement, 'keyup')
.map((e:any) => e.target.value)
.debounceTime(450)
.concat()
.distinctUntilChanged()
.subscribe(item => this.keywordChange.emit(item));
}
onDestroy(){
this.subscription.unsubscribe();
}RxJS is not so simple
Use the following:
- take(n): passes through N values before Observable is complete.
- takeWhile(predicate): checks if predicate matches false any time a new value passes through a pipe, Observable complete if true.
- first(): returns the first value passes through a pipe, completes an Observable.
- takeUntil(Observable): closes a pipe when an inner Observable emits a value
- Subscriptions: acts like a disposable bag, collects subscriptions, then drops all of them
Cause:
- easy to maintain code
- less subscriptions to take care about
Deep dive into RXJS
Deep dive into....
take ()

export function take<T> count: number): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => {
if (count === 0) {
return empty();
} else {
return source.lift(new TakeOperator(count));
}
};
}private nextOrComplete(value: T, predicateResult: boolean): void {
const destination = this.destination;
if (Boolean(predicateResult)) {
destination.next(value);
} else {
destination.complete();
}
}Deep dive into...
take ()
Looks fine
Memory leak
Observable.from([1,2,3])
.filter(value => false)
.take(0)
.subscribe(console.log)Observable.from([1,2,3])
.filter(value => false)
.take(1)
.subscribe(console.log)takeWhile ()

protected _next(value: T): void {
const destination = this.destination;
let result: boolean;
try {
result = this.predicate(value, this.index++);
} catch (err) {
destination.error(err);
return;
}
this.nextOrComplete(value, result);
}
private nextOrComplete(value: T, predicateResult: boolean): void {
const destination = this.destination;
if (Boolean(predicateResult)) {
destination.next(value);
} else {
destination.complete();
}
}Deep dive into...
takeWhile ()
in case of a situation when nothing passes through the pipe, right after isDestroyed is set to true, subscription will stay alive
takeWhile classes the pipe only in case isDestroyed= true at the moment when something passes through the pipe
isDestroyed = false;
listenAndSuggest(){
return Observable
.fromEvent(this.element.nativeElement, 'keyup')
.takeWhile(() => this.isDestroyed)
.subscribe(console.log);
}
onDestroy(){
this.isDestroyed = true;
}Утечка памяти
Deep dive into...
first()
But...

protected _complete(): void {
const destination = this.destination;
if (!this.hasCompleted && typeof this.defaultValue !== 'undefined') {
destination.next(this.defaultValue);
destination.complete();
} else if (!this.hasCompleted) {
destination.error(new EmptyError);
}
}If called with no arguments, 'first' emits the first value of the sourse Observable, then completes. If called with a 'predicate' function, 'first' emits the first value of the source that matches the specified condition.
Deep dive into...
takeUntil ()
Pretty hones method,

notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.complete();
}- takeUntil subscribes and begins mirroring the source Observable. It also
- monitors a second Observable, notifier that you provide. If the notifier
- emits a value, the output Observable stops mirroring the source Observable
- and completes. If the notifier doesn't emit any value and completes
- then takeUntil will pass all values.
Deep dive into...
The place where unsubscribe also metters
const timerOne = Rx.Observable.timer(1000, 4000);
const timerTwo = Rx.Observable.timer(2000, 4000)
const timerThree = Rx.Observable.timer(3000, 4000)
const combined = Rx.Observable
.timer(2, 1000)
.pipe(takeUntil(this.componentDestroy))
.merge(
timerOne,
timerTwo,
timerThree,
)
.subscribe(console.log) --> 0 0 1 2 3 4 1 5 6 7 8 2 9 10 11 12 3 13 14 15 16Memory leak
merge generates a new Observable, takeUntil gonna close everything until merge
const timerOne = Rx.Observable.timer(1000, 4000);
const timerTwo = Rx.Observable.timer(2000, 4000)
const timerThree = Rx.Observable.timer(3000, 4000)
const combined = Rx.Observable
.timer(2, 1000)
.merge(
timerOne,
timerTwo,
timerThree,
).pipe(
takeUntil(this.componentDestroy)
)
.subscribe(console.log) --> 0 0 1 2 3 4 1 5 6 7 8 2 9 10 11 12 3 13 14 15 16Sounds good
merge will be also completed
The place where unsubscribe also metters
Using decorators or
ng-neat@autounsubscribe
takeUntil() gonna save the world
import { Subject } from 'rxjs/Subject';
export function TakeUntilDestroy(constructor: any) {
const originalNgOnDestroy = constructor.prototype.ngOnDestroy;
constructor.prototype.componentDestroy = function () {
this._takeUntilDestroy$ = this._takeUntilDestroy$ || new Subject();
return this._takeUntilDestroy$.asObservable();
};
constructor.prototype.ngOnDestroy = function () {
if (this._takeUntilDestroy$) {
this._takeUntilDestroy$.next(true);
this._takeUntilDestroy$.complete();
}
if (originalNgOnDestroy && typeof originalNgOnDestroy === 'function') {
originalNgOnDestroy.apply(this, arguments);
}
};
}
In case you did it wrong...
RxJS and Angular?



18к
DETACHED NODES and angular
Angular Router Reuse Strategy
- Default strategy is to reuse the rout

id: 1
'/ route / 1'
'/ route / 2'




id: 1
'/ route / 1'
'/ route / 2'

id: 2




ANGULAR ROUTER REUSE STRATEGY
ES6 WeakSet




Going to a place where Angular adds components to a tree

Redux
Redux
Redux is one of the hottest libraries in front-end
Redux is a predictable state container for JavaScript apps
Redux is one of the hottest libraries in front-end

Redux is a single source of truth
Redux. Actions
Actions are plain JavaScript objects that describe WHAT happened, but don’t describe HOW the app state changes.
We just dispatch (send) them to our store instance whenever we want to update the state of our application
{
type: ADD_NOTE,
payload: {
content: 'This is an action object'
}
}we are not handling any logic about how the store changes
Redux. Reducers
Reducers are pure functions that define HOW the app state changes. In other words, they are used to recalculate the new application state or, at least a part of it
(previousState, action) => newStateTo deal with reducer complexity, we chunk them down in multiple, simpler reducers and later, we combine them with a Redux helper function called combineReducers
Redux. Data-Flow
1. The button click handler function dispatches an action to the store with the store.dispatch() method
2. Redux passes down the dispatched action to the reducer
3. The store saves the new state returned by the reducer
4. Since we have subscribed to the store, the function we provided will be called and it will update the UI accordingly
But what to do with side effects?
Redux. Side-Effects
That process of calling into the real world is what side-effects are. They are a way of bridging the pure Redux world with the outside world
Side-effects can happen in response to Redux actions. For example, when a user clicks “Save,” you may want to fire off an AJAX request.
Side-effects may dispatch Redux actions. Like when the save process finishes successfully, you may want to dispatch SAVE_SUCCEEDED; or when it failed, SAVE_FAILED.
They also may not dispatch anything. Some side-effects don’t need to dispatch anything: if you are doing analytics tracking based on Redux actions, you would track things in response to Redux actions, but you will not dispatch anything.
Redux. Side-Effects
- Effects isolate side effects from components, allowing for more pure components that select state and dispatch actions.
- Effects are long-running services that listen to an observable of every action dispatched from the Store.
- Effects filter those actions based on the type of action they are interested in. This is done by using an operator.
- Effects perform tasks, which are synchronous or asynchronous and return a new action.
Smart vs Dumb
Smart vs Dumb

Smart vs Dumb
Smart vs Dumb. Data Flow

Smart vs Dumb. Data Flow with Redux

Data Flow. Getting deeper

Data Flow. Getting deeper

NgRX store
Actions
const action = createAction('[Entity] simple action');
action();const action = createAction('[Entity] simple action', props<{ name: string, age: number, }>());
action({ name: 'andrei', age: 18 });const action = createAction('action',(u: User, prefix: string) => ({ name: `${prefix}${u.name}` }) );
const u: User = { /* ... */ };
action(u, '@@@@');What is inside?
function defineType<T extends string>(
type: T,
creator: Creator
): ActionCreator<T> {
return Object.defineProperty(creator, 'type', {
value: type,
writable: false,
});
}NgRX store
Reducers
export interface ActionReducer<T, V extends Action = Action> {
(state: T | undefined, action: V): T;
}const REDUCERS_TOKEN = new InjectionToken('REDUCERS');
@NgModule({
imports: [
StoreModule.forRoot(REDUCERS_TOKEN)
],
providers: [
{ provide: REDUCERS_TOKEN, useValue: { foo: fooReducer } }
],
}) /* ... */StoreModule.forRoot({ foo: fooReducer, user: UserReducer })Providing reducers
- an object whose values are reducers created with the help of createReducer
- an injection token
NgRX store
How are reducers set up?
StoreModule.forRoot({ entity: entityReducer })/* ... */
{
provide: _REDUCER_FACTORY,
useValue: config.reducerFactory
? config.reducerFactory
: combineReducers,
},
{
provide: REDUCER_FACTORY,
deps: [_REDUCER_FACTORY, _RESOLVED_META_REDUCERS],
useFactory: createReducerFactory,
},
/* ... */StoreModule.forRoot will return a ModuleWithProviders object which contains, among others, these providers:
export class ReducerManager /* ... */ {
constructor(
@Inject(INITIAL_STATE) private initialState: any,
@Inject(INITIAL_REDUCERS) private reducers: ActionReducerMap<any, any>,
@Inject(REDUCER_FACTORY)
private reducerFactory: ActionReducerFactory<any, any>
) {
super(reducerFactory(reducers, initialState));
}
/* ... */
}The REDUCER_FACTORY token will only be injected in ReducerManager class:
NgRX store
Why a pure function?
export function combineReducers(
reducers: any,
initialState: any = {}
): ActionReducer<any, Action> {
const reducerKeys = Object.keys(reducers);
const finalReducers: any = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key];
}
}
/*
Remember from the previous snippet: `const reducer = reducerFactory(reducers)`
Now, the `reducer` will be the below function.
*/
return function combination(state, action) {
state = state === undefined ? initialState : state;
let hasChanged = false;
const nextState: any = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer: any = finalReducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? nextState : state;
};
}If a reducer returned the same reference of an object, but with a property changed, this would not be reflected into the UI as nextStateForKey !== previousStateForKey would fail
NgRX store
Reducer
const increment = createAction('increment');
const decrement = createAction('decrement');
const reset = createAction('reset');
const _counterReducer = createReducer(initialState,
on(increment, state => state + 1 /* reducer#1 */),
on(decrement, state => state - 1 /* reducer#2 */),
on(reset, state => 0 /* reducer#3 */),
);
export function counterReducer(state, action) {
return _counterReducer(state, action);
}export function on<C1 extends ActionCreator, S>(
creator1: C1,
reducer: OnReducer<S, [C1]>
): On<S>;
/* ... Overloads ... */
export function on(
...args: (ActionCreator | Function)[]
): { reducer: Function; types: string[] } {
const reducer = args.pop() as Function;
const types = args.reduce(
// `creator.type` is a property directly attached to the function so that
// it can be easily accessed(`createAction` is responsible for that)
(result, creator) => [...result, (creator as ActionCreator).type],
[] as string[]
);
return { reducer, types };
}The on functions are an alternative for using the switch statement.
NgRX store
The on function can bind a reducer to multiple actions. Then, in the reducer, with the help of discriminated unions, we can perform the appropriate state change depending on action.
const a1 = createAction('a1', props<{ name: string }>());
const a2 = createAction('a2', props<{ age: number }>());
const initialState = /* ... */;
const reducer = createReducer(
initialState,
on(a1, a2, (state, action) => {
if (action.type === 'a1') {
action.name
} else {
action.age
}
}),
)export function on<C1 extends ActionCreator, C2 extends ActionCreator, S>(
creator1: C1,
creator2: C2,
reducer: OnReducer<S, [C1, C2]>
): On<S>;
// `C[number]` will result in a union
export interface OnReducer<S, C extends ActionCreator[]> {
(state: S, action: ActionType<C[number]>): S;
}export function createReducer<S, A extends Action = Action>(
initialState: S,
...ons: On<S>[]
): ActionReducer<S, A> { /* ... */ }NgRX store
Using factory, not on
import { Action, ActionReducer } from '@ngrx/store';
export interface ActionReducers<S> { [action: string]: ((p: any, s: S) => (S | ((s: S) => S))); }export interface GenericAction extends Action {
payload?: any;
}export const wrappingFunction = (maybeFunction: Function | any, ...args) => {
return typeof maybeFunction === 'function'
? maybeFunction(...args)
: maybeFunction;
}export const reducingFunction = <S>(
actionReducers: ActionReducers<S>,
state: S,
action: GenericAction,
): S => {
const reducingFunc = actionReducers[action.type];
return reducingFunc
? wrappingFunction(reducingFunc(action.payload, state), state)
: state;
}NgRX store
Using factory, not on
export const initialState: State = {
someValue: null,
};
export const actionReducers: ActionReducers<FormState> = {
[FEATURE.AREA.ACTION]: () => initialFormState,
[FEATURE.AREA.ACTION]: ({
value,
}) =>
assoc('someValue', value),
};
export const sampleReducer = (state = initialState, action) => {
return reducingFunction<State>(actionReducers, state, action);
}NgRX store
The store
export class Store<T> extends Observable<T> implements Observer<Action> {
constructor(
state$: StateObservable,
private actionsObserver: ActionsSubject,
private reducerManager: ReducerManager
) {
super();
this.source = state$;
}
/* ... */
}const s = new Subject();
class Custom extends Observable<any> {
constructor () {
super();
// By doing this, every time you do `customInstance.subscribe(subscriber)`,
// the subscriber wll be part of the subject's subscribers list
this.source = s;
}
}
const obs$ = new Custom();
// The subject has no subscribers at this point
s.next('no');
// The subject has one subscriber now
obs$.subscribe(console.log);
// `s.next()` -> sending values to the active subscribers
timer(1000)
.subscribe(() => s.next('john'));
timer(2000)
.subscribe(() => s.next('doe'));dispatch<V extends Action = Action>(
action: V /* ... type check here - skipped */
) {
this.actionsObserver.next(action);
}EffectsModule.forRoot([effectClass]), EffectsModule.forFeature([effectClass]) or the USER_PROVIDED_EFFECTS multi token
{
return {
ngModule: EffectsRootModule,
providers: [
{
// Make sure the `forRoot` static method is called only once
provide: _ROOT_EFFECTS_GUARD,
useFactory: _provideForRootGuard,
deps: [[EffectsRunner, new Optional(), new SkipSelf()]],
},
EffectsRunner,
EffectSources,
Actions,
rootEffects, // The array of effects
{
// Dependency for `ROOT_EFFECTS`
provide: _ROOT_EFFECTS,
// Providing it as an array because of how `createEffects` is implemented
useValue: [rootEffects],
},
{
// This token would be provided by the user in its separate module
provide: USER_PROVIDED_EFFECTS,
multi: true,
useValue: [], // [UserProvidedEffectsClass]
},
{
provide: ROOT_EFFECTS,
useFactory: createEffects,
deps: [Injector, _ROOT_EFFECTS, USER_PROVIDED_EFFECTS],
},
],
};
}
export const = createEffects => (
injector: Injector,
effectGroups: Type<any>[][],
userProvidedEffectGroups: Type<any>[][]
): any[] {
const mergedEffects: Type<any>[] = [];
for (let effectGroup of effectGroups) {
mergedEffects.push(...effectGroup);
}
for (let userProvidedEffectGroup of userProvidedEffectGroups) {
mergedEffects.push(...userProvidedEffectGroup);
}
return createEffectInstances(injector, mergedEffects);
}
// Here the instances are created
export const createEffectInstances = (/* ... */): any[] =>
effects.map(effect => injector.get(effect);
@NgModule({})
export class EffectsRootModule {
constructor(
private sources: EffectSources,
runner: EffectsRunner,
store: Store<any>,
@Inject(ROOT_EFFECTS) rootEffects: any[],
/* ... */
) {
// Subscribe to the `effects stream`
// The `observer` is the Store entity
runner.start();
rootEffects.forEach(effectSourceInstance =>
// Push values into the stream
sources.addEffects(effectSourceInstance)
);
store.dispatch({ type: ROOT_EFFECTS_INIT });
}
addEffects(effectSourceInstance: any) {
this.sources.addEffects(effectSourceInstance);
}
}EffectsRootModule will inject ROOT_EFFECTS which will contain the needed instances and will push them into the effects stream
Data Flow. Getting deeper
createReducer(
initialState,
on(rootEffectsInit, (s, a) = {/* ... */})
)perform specific state changes when the root effects are initialized by registering the rootEffectsInit action in a reduce
Effects. Getting deeper
@NgModule({})
export class EffectsFeatureModule {
constructor(
// Make sure the essential services(EffectsRunner, EffectSources)
// are initialized first
root: EffectsRootModule,
@Inject(FEATURE_EFFECTS) effectSourceGroups: any[][],
/* ... */
) {
effectSourceGroups.forEach(group =>
group.forEach(effectSourceInstance =>
root.addEffects(effectSourceInstance)
)
);
}
}the EffectsRootModule does a bit more than just instantiating the effects
Effects. Getting deeper
@NgModule({})
export class EffectsRootModule {
constructor(
private sources: EffectSources,
runner: EffectsRunner,
store: Store<any>,
@Inject(ROOT_EFFECTS) rootEffects: any[],
@Optional() storeRootModule: StoreRootModule,
@Optional() storeFeatureModule: StoreFeatureModule,
@Optional()
@Inject(_ROOT_EFFECTS_GUARD)
guard: any
) {
runner.start(); // Creating the stream
rootEffects.forEach(effectSourceInstance =>
sources.addEffects(effectSourceInstance)
);
store.dispatch({ type: ROOT_EFFECTS_INIT });
}
addEffects(effectSourceInstance: any) {
// Pushing values into the stream
this.sources.addEffects(effectSourceInstance);
}
}@Optional() storeRootModule: StoreRootModule and @Optional() storeFeatureModule: StoreFeatureModule will make sure that effects are initialized after the ngrx/store entities (the results of StoreModule.forRoot() and eventually StoreModule.forFeature()) have been initialized.
This beforehand initialization includes:
Effects. Getting deeper. Initialization
- the creation of the reducers object: all the registered reducers, those from feature modules as well, will be merged into one big object that will represent the shape of the app
- the State entity - where the app information is kept, also where the place actions meet reducers, meaning it's where reducers being invoked, which may cause state changes
- the Store entity - the middleman between the data consumer(e.g: a smart component) and the model(the State entity)
- the ScannedActionsSubject - the stream that the effects (indirectly) subscribe to
Effects. Getting deeper.
Store runner
// EffectsRunner
start() {
if (!this.effectsSubscription) {
this.effectsSubscription = this.effectSources
.toActions()
.subscribe(this.store);
}
}export class Store<T = object> extends Observable<T>
implements Observer<Action> {
next(action: Action) {
this.actionsObserver.next(action);
}
}Effects. Getting deeper.
The entire flow

e - effect
M - Observable of merged effects
I - Observable that calls ngrxOnInitEffects
A - Resulted merged actions
Effects. Getting deeper.
Creation of an effect
type DispatchType<T> = T extends { dispatch: infer U } ? U : true;
type ObservableType<T, OriginalType> = T extends false ? OriginalType : Action;
export function createEffect<
C extends EffectConfig,
DT extends DispatchType<C>,
OT extends ObservableType<DT, OT>,
R extends Observable<OT> | ((...args: any[]) => Observable<OT>)
>(source: () => R, config?: Partial<C>): R & CreateEffectMetadata {
const effect = source();
const value: EffectConfig = {
...DEFAULT_EFFECT_CONFIG,
...config, // Overrides any defaults if values are provided
};
Object.defineProperty(effect, CREATE_EFFECT_METADATA_KEY, {
value,
});
return effect as typeof effect & CreateEffectMetadata;
}createEffect() will return an observable with a property CREATE_EFFECT_METADATA_KEY attached to it which will hold the configuration object for that particular effect
const observable$: Observable<any> =
typeof sourceInstance[propertyName] === 'function'
? sourceInstance[propertyName]()
: sourceInstance[propertyName];
const effectAction$ = useEffectsErrorHandler
? effectsErrorHandler(observable$, globalErrorHandler)
: observable$;
if (dispatch === false) {
return effectAction$.pipe(ignoreElements());
}The ignoreElements operator will ignore everything, except error or completenotifications.
Effects. Getting deeper.
Creation of an effect
addUser$ = createEffect(
() => this.actions$.pipe(
ofType(UserAction.add),
exhaustMap(u => this.userService.add(u)),
map(/* Map to action */)
),
)If an error occurs due to calling userService.add() and it is not handled anywhere, like
// `this.userService.add(u)` is a cold observable
exhaustMap(
u => this.userService.add(u).pipe(catchError(err => /* Action */))
),
addUser$ will unsubscribe from the actions$ stream. defaultEffectsErrorHandler will simply re-subscribe to actions$, but there's another thing that's worth mentioning: the actions$ stream is actually a Subject so we know for sure that when re-subscribed, we won't receive any of the previously emitted values, only the newer ones
{
provide: EFFECTS_ERROR_HANDLER,
useValue: customErrHandler,
},
function customErrHandler (obs$, handler) {
return obs$.pipe(
catchError((err, caught$) => {
console.log('caught!')
// Only re-subscribe once
// return obs$;
// Re-subscribe every time an error occurs
return caught$;
}),
)
}Effects. Getting deeper.
Creation of an effect
addUser$ = createEffect(
() => this.actions$.pipe(
ofType(UserAction.add),
exhaustMap(u => this.userService.add(u)),
map(/* Map to action */)
),
)If an error occurs due to calling userService.add() and it is not handled anywhere, like
// `this.userService.add(u)` is a cold observable
exhaustMap(
u => this.userService.add(u).pipe(catchError(err => /* Action */))
),
addUser$ will unsubscribe from the actions$ stream. defaultEffectsErrorHandler will simply re-subscribe to actions$, but there's another thing that's worth mentioning: the actions$ stream is actually a Subject so we know for sure that when re-subscribed, we won't receive any of the previously emitted values, only the newer ones
{
provide: EFFECTS_ERROR_HANDLER,
useValue: customErrHandler,
},
function customErrHandler (obs$, handler) {
return obs$.pipe(
catchError((err, caught$) => {
console.log('caught!')
// Only re-subscribe once
// return obs$;
// Re-subscribe every time an error occurs
return caught$;
}),
)
}Meta Reducers
Meta-reducers are functions that receive a reducer and return a reducer
export class StoreModule {
static forRoot(
reducers,
config: RootStoreConfig<any, any> = {}
): ModuleWithProviders<StoreRootModule> {
return {
ngModule: StoreRootModule,
providers: [
/* ... */
{
provide: USER_PROVIDED_META_REDUCERS,
useValue: config.metaReducers ? config.metaReducers : [],
},
{
provide: _RESOLVED_META_REDUCERS,
deps: [META_REDUCERS, USER_PROVIDED_META_REDUCERS],
useFactory: _concatMetaReducers,
},
{
provide: REDUCER_FACTORY,
deps: [_REDUCER_FACTORY, _RESOLVED_META_REDUCERS],
useFactory: createReducerFactory,
},
/* ... */
]
}
}
}RESOLVED_META_REDUCERS when injected in createReducerFactory, it will be an array resulted from merging the built-in meta-reducers with the custom ones
Meta Reducers
Meta-reducers are functions that receive a reducer and return a reducer
export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
return function(state, action) {
console.log('state', state);
console.log('action', action);
return reducer(state, action);
};
}
export const metaReducers: MetaReducer<any>[] = [debug];
NgRX. Selectors
this.store.select('pizzas');this.store.select(state => state.pizzas);export const getProductsState = createFeatureSelector('products');createFeatureSelector allows us to get a top-level feature state property of the state tree simply by calling it out by its feature name:
export interface ProductsState {
pizzas: fromPizzas.PizzaState;
toppings: fromToppings.ToppingsState;
}
export const getProductsState = createFeatureSelector<ProductsState>('products');
export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);NgRX. Selectors
state = {
// ProductState
products: {
// PizzaState
pizzas: {
entities: {},
loaded: false,
loading: true,
},
// ToppingsState
toppings: {
entities: {},
loaded: false,
loading: true,
},
},
};state -> products -> pizzas -> entities// src/products/store/reducers/index.ts
export interface ProductsState {
pizzas: fromPizzas.PizzaState;
toppings: fromToppings.ToppingsState;
}
export const getProductsState = createFeatureSelector<ProductsState>('products');
export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);NgRX. Selectors
export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);
export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);
export const getAllPizzas = createSelector(getPizzasEntities, entities => {
return Object.keys(entities).map(id => entities[id]);
});NgRX. Selectors
export const getPizzaState = createSelector(
getProductsState,
(state: ProductsState) => state.pizzas
);
export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);export const getPizzasEntities = createSelector(
getPizzaState,
fromPizzas.getPizzasEntities
);
export const getAllPizzas = createSelector(getPizzasEntities, entities => {
return Object.keys(entities).map(id => entities[id]);
});NgRX. Resolvers
@Injectable()
export class AsyncResolver implements Resolve<any> {
constructor(private store: Store<AppState>, private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
const currentDate = Date.now();
return combineLatest(
this.store.select(getAllCampaignsStatus),
this.store.select(getLastChecked),
this.store.select(getAsyncUpdateStatus),
of(currentDate),
).pipe(
tap(([campaignsStatus]) => {
if (!campaignsStatus.loaded && !campaignsStatus.loading) {
this.store.dispatch(new LoadAllCampaigns());
}
}),
filter(([campaignsStatus]) => campaignsStatus.loaded),
tap(([campaignsStatus, lastChecked, status, now]) => {
// isChecked is required to not accidentally emit excessive event
const isChecked = isNotNil(lastChecked) && lastChecked >= now;
if (!status.loading && !isChecked) {
this.store.dispatch(new CheckAsyncStatus());
}
}),
filter(isNotNil(lastChecked)),
take(1)
);
}
}
Спасибо за Внимание!
NgRX: In Depth
By Yauheni Pozdnyakov
NgRX: In Depth
- 178