Skip to content

Dispatch multiple actions at once #468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ghetolay opened this issue Oct 9, 2017 · 18 comments
Closed

Dispatch multiple actions at once #468

ghetolay opened this issue Oct 9, 2017 · 18 comments

Comments

@ghetolay
Copy link

ghetolay commented Oct 9, 2017

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.

@Szpadel
Copy link
Contributor

Szpadel commented Oct 10, 2017

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]) );

@ghetolay
Copy link
Author

ghetolay commented Oct 10, 2017

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) )
  );

@Szpadel
Copy link
Contributor

Szpadel commented Oct 10, 2017

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();

@brandonroberts
Copy link
Member

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

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

@ghetolay
Copy link
Author

ghetolay commented Oct 11, 2017

@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.

@brandonroberts
Copy link
Member

@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

@ghetolay
Copy link
Author

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.

@progral
Copy link
Contributor

progral commented Jan 13, 2018

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

@spock123
Copy link

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

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

@progral
Copy link
Contributor

progral commented Jan 13, 2018

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);
        }
    };
}

@progral
Copy link
Contributor

progral commented Jan 14, 2018

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>) {]
}

@progral
Copy link
Contributor

progral commented Jan 15, 2018

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

@FortinFred
Copy link

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

Could the batching be usefull there as well?

@rossanmol
Copy link

rossanmol commented May 10, 2018

@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?

@christophechevalier
Copy link

@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.

@timosadchiy
Copy link

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)

@Hainesy
Copy link

Hainesy commented Feb 26, 2019

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.

@manklu
Copy link

manklu commented Feb 27, 2019

@Hainesy Something like this should work.

this.actions$.pipe(
  ofType('BatchAction'),
  concatMap(action => from(action.payload)),
  ofType('EffectAction'),
  ...
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests