Store: How do you cancel a previous action handler (like a switchMap in ngrx effects)

Created on 23 Mar 2018  路  14Comments  路  Source: ngxs/store

Versions

  • ngxs: v2.0-rc20

Observed behavior

I read the docs and cannot see how this could work with the current action handlers

Desired behavior

This library seems to have a great approach to simplifying the Redux pattern and bringing it to devs without terrifying newcomers. So I am very interested in the possibility of using it in a very large Angular app that uses NgRx and the dev team taking over the app are terrified of the Redux paradigm and especially rxjs.
My one concern is that in reading through the docs and looking at how the action handlers are declared and coded I can't see how to achieve the equivalent of a switchMap in an Effect in NgRx.
We need a way to circumvent the issue described here by Victor Savkin.
This is a blocker for my app as there are many times that an action is dispatched again to override the previous query from the server.

This is just one use case that unfortunately doesn't seem to be supported by the current Action Handler approach. Another one for example would be a debounce or throttling of an action type in the stream. It feels like we need the richness that RxJs provides without the complexity.

Proposal

So here are my thoughts on how this could work...
We could provide an optional second parameter to the @Action decorator that provides a pipeable rxjs operator to use to execute the action handler... I'm sure you may come up with something simpler though ;-) This is what I am thinking, something like:

@Action(FeedAnimals, switchMap({handler, context, action} => handler(context, action)))
  feedAnimals({ getState, setState }: StateContext<ZooStateModel>, { payload }: FeedAnimals) { 
  ...
  }

That standard switchMap pipeable operator could be included in the library for convenience and referenced as cancelPrevious or something like that. So the action decorator could become:
@Action(FeedAnimals, cancelPrevious)
I really don't know how feasible this is because I have just had a look at your state-factory.ts and you don't currently seem to use rxjs to dispatch the action handlers.
My example above also doesn't make too much sense regarding how it applies the pipeable operator in order to manipulate the previous handler, but hopefully you get the idea I am trying to communicate.

An alternative could be more in the way of options:
@Action(FeedAnimals, { cancelPrevious: true, debounce: 100 })
This could then be applied internally in whatever way makes sense.
Potentially when the next action comes in it could short circuit the setState, patchState, and dispatch on the context of the previous action handler call so that when they get called they have no effect.
Options would make things more explicit but less flexible.

I hope this makes sense.

core

Most helpful comment

Perhaps a source observable filtered to the supplied actions could be injected it into the function (like a custom operator).

@Action(FeedAnimals)
  feedAnimals(state: StateContext<ZooStateModel>, source$: Observable<FeedAnimals>) { 
    return source$.pipe(
      switchMap(({ payload }) => ...)
    )
  }

As a side note, what I like about the @Effect() decorator in ngrx is that it doesn't have to originate from an action; you can trigger actions or other side effects with any observable. Some of the expressiveness of observables gets lost by wrapping async actions in this way.

All 14 comments

Thanks @bossqone. I searched the issues before posting and still didn't find your issue. This problem can be described so many ways!

Perhaps a source observable filtered to the supplied actions could be injected it into the function (like a custom operator).

@Action(FeedAnimals)
  feedAnimals(state: StateContext<ZooStateModel>, source$: Observable<FeedAnimals>) { 
    return source$.pipe(
      switchMap(({ payload }) => ...)
    )
  }

As a side note, what I like about the @Effect() decorator in ngrx is that it doesn't have to originate from an action; you can trigger actions or other side effects with any observable. Some of the expressiveness of observables gets lost by wrapping async actions in this way.

This issue has wreaked my weekend. I have not been able to turn my mind off because there must be a cleaner way to express this. I was supposed to be relaxing!

Ok, so I have realized that I was trying to fit the 'old' ngrx reactive paradigm in here somehow but was not thinking of how the underlying pattern is different here.
This pattern is essentially a strategy/visitor pattern (with some interesting async super powers). So, why don't we approach this using a similar approach to the ngxs plugins.
What I am thinking is that we need to be able intercept and control the execution, inputs and outputs of the action handler. This sounds like a Chain of Responsibility design pattern.

What I propose is that we could have the ability to add classes into the pipeline that processes the request.
These could be called one of the following: ActionFilter, ActionInterceptor, ActionHandler, ActionPipe
I think that I prefer ActionPipe because it fits that paradigm so I will use that word going forward.
I propose that the code could look like this:

@Action(FeedAnimals, [ LatestCallWins ])
feedAnimals({ getState, setState }: StateContext<ZooStateModel>, { payload }: FeedAnimals) { 
  ...
  }

I deliberately tried to keep the LatestCallWins name expressive but it could also be Cancellable, SwitchMap, CancelsPrevious, LatestCallCancelsPrevious or similar. You would be able to list multiple Action Pipes in the same fashion as providers on a module (by type, by factory or by value).
These Action Pipes would look like this pseudo code:

interface IActionPipe {
  setNext(next: IActionPipe): void;
  process(context: StateContext, action: Action): any;
}

class LatestCallWins implements IActionPipe  {
  private next: IActionPipe = null;
  private previousCallContext: StateContext = null;

  setNext(next: IActionPipe) { this.next = next; }

  process(context: StateContext, action: Action) {
    if (this.previousCallContext && this.previousCallContext.cancel) {
      this.previousCallContext.cancel();
    }
    let callContext = this.createCancellableCallContext(context);
    this.previousCallContext = callContext;
    return this.next.process(callContext, action)
  }

  createCancellableCallContext(context: StateContext): StateContext {
    var cancelled = false;
    return {
      setState: function(value) { if(!cancelled) context.setState(value); },
      getState: function() { return context.getState(); },
      patchState: function(value) { if(!cancelled) context.patchState(value); },
      dispatch: function(action) { if(!cancelled) context.dispatch(action); },
      cancel: function() { cancelled = true; }
    };
  }
}

We could define ones for Debouncing, Throttling, Transformations, and Extensions.
The process function could even look different if the ActionPipe is able to call it.
For Example you could have:

@Action(FeedAnimals, [ ActionMap ])
feedAnimals({ payload }: FeedAnimals) { 
    return [ new FeedCows(payload.hay), new FeedChickens(payload.seeds) ];
  }

Here the ActionMap pipe would expect a function that returns one or many actions and it would dispatch those automatically.

I'm quite excited about the prospects of this! Hopefully I will get a chance to create a pull request for this soon, unless somebody beats me to it... or unless this idea gets shot down ;-)

Dev team, what do you think? @amcdnl @deebloo (not sure others?)

Wow, thanks for the detail there, its excellent!

So, we still use observable streams under the hood and since every dispatch returns an observable (whether you do it yourself or not - https://github.com/ngxs/store/blob/master/src/store.ts#L83 ). We already have a action stream ( https://github.com/ngxs/store/blob/master/src/store.ts#L94 ), right now we are just using that for external users to be able to tap into the event stream but we should leverage that in the inner workings and we should be able to achieve this with that.

I've got this planned to look into for 2.1 but if you wanna take it, i'd be more than happy to work with you on it.

_PS. @amcdnl Hmmm... I think that I am diverging the discussion from the issue a bit now. Potentially I should create a new issue or rename the issue to "Extensibility for Action Handlers" or similar?_

It would be great to work on this with you.
I will have to see how my time goes, but I will most likely get time to work on this in 2 weeks time.

My mind has still been running with this. I think that this approach can be really useful as an extensibility point. As an example of this, here are some other plugin possibilities that I have thought of if we enable this Action Pipe approach:

Utility

AsReducer

@Action(FeedAnimals, [ AsReducer ])
feedAnimals(state: ZooStateModel, { payload }: FeedAnimals) { 
  return { ...state, feed: payload };
}

AsStatePatch

@Action(FeedAnimals, [ AsStatePatch ])
feedAnimals({ payload }: FeedAnimals) { 
  return { feed: payload };
}

Entity Loader Plugin

EntityLoader

@Action(LoadAnimals, [ LatestCallWins, EntityLoader<AnimalLoadApi>])
loadAnimals(state: ZooStateModel, action: LoadAnimals, apiResponse: Animal[]) { 
  return { ...state, animals: apiResponse};
}

NgRx Migration Plugin

AsNgRxReducer (the same as AsReducer but provided to be explicit about how the ngrx one translates)

@Action(null, [ AsNgRxReducer ])
feedAnimals(state: ZooStateModel, action: Action) { 
  return { ...state, feed: payload };
}

or reference the ngrx reducer directly because the signature is the same

@Action(FeedAnimals, [ AsNgRxReducer ])
feedAnimals = someImport.existingNgrxReducerFn;

AsNgRxEffect

@Action(FeedAnimals, [ AsNgRxEffect ])
feedAnimals(actions$: Actions) { 
  return actions$
    .switchMap( ({payload}) => 
      feedApi
        .doFeeding(payload)
        .map((result) => new FeedingDone(result) )
     );
}

or directly adapt existing ngrx effects (could even do a type alias for this action decorator as @Effect)

@Action(null, [ AsNgRxEffect ])
feedAnimals$ = actions$
    .ofType<FeedAnimals>()
    .switchMap( ({payload}) => 
      feedApi
        .doFeeding(payload)
        .map((result) => new FeedingDone(result) )
     );

This would even handle the case where something else triggers the action (like a timer) as @stupidawesome raised:

@Action(null, [ AsNgRxEffect ])
feedAnimals$ = Observable
    .interval(24 * 60 * 60 * 1000) // 24 hours
    .map(() => new FeedAnimals());

@markwhitfeld I am curious if this will work. So we expose an action stream and an ofAction operator. Would this work if you use takeUntil?

Ex:

  @Action(RemoveTodo)
  removeTodo(state: StateContext<string[]>, action: RemoveTodo) {
    return timer(5000).pipe(
      tap(() => {
        state.setState(
          state.getState().filter((_, index) => index !== action.index)
        );
      }),
      takeUntil(this.actions.pipe(ofAction(RemoveTodo)))
    );
  }

I think this would cancel your previous observable when a new one came in. If this works as a solution we can add it to the docs

@deebloo - What do you think about adding options to actions to handle this automatically. Lets say something like:

@Action(RemoveTodo, { cancelable: true })
remove() { ... }

Not sure if it is. Needed but if we can do it for the user maybe it is worth it

@deebloo @amcdnl Please, whatever you do, I keep it as simple as possible

The thing I said is possible today. The other would be a future enhancement

I'll take this item and add that API.

@amcdnl should we close this ticket now that the initial issue has been answered? We can create a new issue just for the proposed helper API

Good idea! :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

garthmason picture garthmason  路  5Comments

am4apps picture am4apps  路  4Comments

paulstelzer picture paulstelzer  路  5Comments

Newbie012 picture Newbie012  路  4Comments

msegmx picture msegmx  路  5Comments