Platform: Dispatch multiple actions at once

Created on 9 Oct 2017  Â·  18Comments  Â·  Source: ngrx/platform

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[X] Feature request
[ ] Documentation issue or request

Add the ability to dispatch multiple actions without emitting a new state for each action but only once all action are processed.

So instead of

store.select(state).subscribe( () => counter++);

store.dispatch( new AddItem(item1) );
store.dispatch( new AddItem(item2) );

//counter == 2

we would get

store.select(state).subscribe( () => counter++);

store.dispatch( [new AddItem(item1), new AddItem(item2)] );

//counter == 1

I've already looked how it could be done, should be easy and I could do it if this is accepted.

Most helpful comment

Dispatching multiple actions is straightforward enough without changing the existing APIs to handle different code paths.

[Action1, Action2].forEach(store.dispatch);

All 18 comments

I'm not sure how it suppose to work, because reducers need to know previous state, therefore you would need to stop emitting updates somewhere after this point.
And I honestly cannot see benefits from that additional complexity.
I think I had similar issue before, and I solved it by creating another action for it:

store.dispatch( new AddItems([item1, item2]) );

It's really easy to implement just need to use reduce (this is redux after all :)) :

state = actions.reduce( (nextState, action) => reducer(nextState, action), state);

I thought that maybe my problem was about my reducer design but it seems not.
The snippet was just meant to illustrate the behavior briefly. My real use case is about a request from a backend with 4 level nesting object that I convert into several actions. Having only 1 action would make my reducers complex.

small example :

interface AuthorAndBooks {
  author: Author;
  books: Book[];
}
declare function getAuthors(): Observable<AuthorAndBooks[]>;

//effects
return getAuthors()
  .mergeMap( authorsNBooks => Observable.of(
    new AddAuthors(authorsNBooks.map( ({author}) => author )),
    ...authorsNBooks.map( ({author, books}) => new AddBooks(books, author.id) )
  );

I'm dealing with this by chaining reducers internally. I have small helper class:

export function alterState<T extends Object>(source: T, change: Partial<T>): T {
  return Object.assign({}, source, change);
}

export class Chain<T> {
  static from<Ts>(value: Ts) {
    return new Chain(value);
  }

  protected constructor(private value: T) {
  }

  map<R>(fn: (value: T) => R): Chain<R> {
    return new Chain(fn(this.value));
  }

  end(): T {
    return this.value;
  }
}

and then inside reducer I can run multiple steps to transform state internally

    return Chain.from(
      alterState(state, {
        products: List.of(...payload)
      }))
      .map(updateSummary)
      .end();

Dispatching multiple actions is straightforward enough without changing the existing APIs to handle different code paths.

[Action1, Action2].forEach(store.dispatch);

@brandonroberts this is no different than my first snippet it'll emit a new state per action dispatched.
Emitting a new state will trigger selectors to recompute and view stuff to update. That's why I want to emit a new state only once all actions are dispatched.

So there will be a real impact in doing store.dispatch(Action1, Action2) instead of [Action1, Action2].forEach(store.dispatch); It's not just syntax.

@ghetolay what you are proposing introduces a new behavior though. Batching multiple actions is better handled in your reducer. This project has an example of batching actions. https://gitlab.com/linagora/petals-cockpit/blob/master/frontend/src/app/shared/helpers/batch-actions.helper.ts#L34-42

Yeah I also had in mind creating an higher order action but it felt like doing
this.store.dispatch({type: 'HIGHORDER', payload: [Action1, Action2]})
Wasn't worth the trouble so I went with a plain array directly.

Batching multiple actions is better handled in your reducer

Doing it on the reducer means doing more complicated reducers. And doing stuff on reducers to reduce state refresh rate feels misplaced. Your link look like a workaround precisely because it's not handled by the Store natively.

The 'batchActions' solution out of the box would be nice.

Any possibility we could use the Redux package Redux-Batch-Actions in Ngrx/Store?

https://github.com/tshelburne/redux-batched-actions

If we break down redux-batched-actions to the basics it just a metareducer and a BatchAction. Thanks to @brandonroberts link to the batch-action helpers I created this little module while I am just sitting on my sofa and playing with my tablet around. So I can not test if this will work. I will try it in the next days if I am back on my development environment. Perhaps someone can try it earlier. @spock123?? :

import { ActionReducer, Action } from "@ngrx/store";

/* 
 * File: batch-action-reducer.ts
 *
 * BatchAction - Use it to dispatch multiple Actions as one
 * 
 * 0. Put this script somewhere in you app
 * 
 * 1. Register enableBatching function as metareducer. Here an example 
 * with storeFreeze included
 * 
 * import {enableBatchReducer} from './somewhere/batch-action-reducer'
 * 
 * export const metaReducers: MetaReducer<AppState>[] = !environment.production ?
 * [storeFreeze, enableBatchReducer] :
 * [enableBatchReducer];
 * 
 * @NgModule({
 *      ...
 *      imports: [
 *          ....
 *          StoreModule.forRoot(appReducer, { metaReducers }),
 *      ]
 *  ...
 * })
 * 
 * 2. Create an anonymous batch action. It has no own action type, 
 *    so we use "Anonymous Batch Action" as default
 * 
 *   this.store.dispatch(new BatchAction([new FooAction(), new BarAction({bar: 'somePayload'})]))
 * 
 * 3. You can make your own BatchAction if you want to dispatch another type,
 *    to be honest a "Anonymous Batch Action" type is very ugly in redux dev tools. 
 *    Perhaps we would like to react on it in our Effects or Reducers, too. So we need a unqiue 
 *    type.
 * 
 * import { BatchAction } from './somewhere//batch-action-reducer
 * 
 * export const MY_ACTION = 'My Action';
 *    
 * export class MyAction extends BatchAction {
 *     readonly type = MY_ACTION;
 *     constructor(payload: any[]) { super(payload) }
 * } 
 * 
 * this.store.dispatch(new MyAction([new FooAction(), new BarAction('somePayload')]));
 */

export class BatchAction implements Action {
    public type = 'Anonymous Batch Action';

    constructor(public payload: any[]) { }
}

export function enableBatchReducer<S>(reduce: ActionReducer<S>): ActionReducer<S> {
    return function batchReducer(state: S, action: Action): S {
        if (action instanceof BatchAction) {
            let batchActions = action.payload;
            return batchActions.reduce(batchReducer, state);
        } else {
            return reduce(state, action);
        }
    };
}

Perhaps a better methode would be with an decorator, but I do not know how to do it.

@BatchAction
class MyAction implements Action {
    readonly type = MY_ACTION;
   constructor(payload: Array<any>) {]
}

After some research I have created my first decorator and my first own project on github for my own suggestion above. As far as I can say it works as aspected. Perhaps someone could play around with it and review my code. Would be great if we could have something like this out of the box. "Batteries included", you know....

ngrx-batch-action-reducer

The question that brought me here is rather an action split effect would produce two states.

Could the batching be usefull there as well?

@brandonroberts that solution won't work after rxjs6 update, as MergeMapOperator is now internal only. Is there any way of keeping same behavior after this upgrade?

@rossanmol .
For now, I'm using this import :
import { MergeMapOperator } from 'rxjs/internal/operators/mergeMap';

Update 'MergeMapOperator' extend parameters

export class ExplodeBatchActionsOperator extends MergeMapOperator<
  Action,
  Action
> { ... }
@Injectable()
export class ActionsWithBatched extends Actions<Action> {
  constructor(@Inject(ScannedActionsSubject) source?: Observable<Action>) {
    super(source);
    // TODO replace deprecated operator attribute. See https://github.com/ngrx/platform/issues/468
    // @deprecated — This is an internal implementation detail, do not use.
    this.operator = explodeBatchActionsOperator();
  }
}

I'm using this version :

...
    "@angular/core": "6.0.1",
    "typescript": "2.7.2",
    "rxjs": "6.0.0"
...

I will reported this issue on my project, and resolve that later may be but I don't have error in my console, just a little part of deprecated code ...

Here the file of bacth actions helper : https://gitlab.com/linagora/petals-cockpit/blob/master/frontend/src/app/shared/helpers/batch-actions.helper.ts#L34-42

I stay tunned at all discussions about this.

FYI syntax suggested by @brandonroberts [Action1, Action2].forEach(store.dispatch); gave me an error TypeError: Cannot read property 'actionsObserver' of undefined. I believe that is due to the dispatch method is not applied to store with that syntax. A minor change works well for me though:

[Action1, Action2].forEach(a => store.dispatch(a));

Here is a helper function that I added to my project to dispatch multiple actions without copying and pasting the above snippet:

export const dispatchFew = <T>(store$: Store<T>) => (...actions: Act[]) => actions.forEach(a => store$.dispatch(a));

// Example of using it
dispatchFew(store$)(Action1, Action2)

I need something similar to what ngrx-batch-action-reducer offers, but a solution that also triggers effects for the individual actions.

It's definitely desirable to be able to batch together a bunch of actions from the point of view of reducers, such that selectors are not triggered after each action; but in such a way that the effects are for each action are still honoured.

@Hainesy Something like this should work.

this.actions$.pipe(
  ofType('BatchAction'),
  concatMap(action => from(action.payload)),
  ofType('EffectAction'),
  ...
)
Was this page helpful?
0 / 5 - 0 ratings