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
  • Subscriberan 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

ofcreates 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 16

Memory 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 16

Sounds 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) => newState

To 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