54

I'm trying to do simple thing - after some entity saved (using http request) I want to navigate back to list route. The problem is : how to subscribe to success action (or may be reducer or effect?)

here is my actions code:

static SAVE_POST = '[POST] Save POST';
savePOST(Post): Action {
    return {
        type: PostActions.SAVE_POST,
        payload: Post
    };
}

static SAVE_POST_SUCCESS = '[POST] Save POST Success';
savePOSTSuccess(Post): Action {
    console.log('action: savePostSuccess')
    return {
        type: PostActions.SAVE_POST_SUCCESS,
        payload:Post
    };
}

i'm using Effects:

   @Effect() savePost$ = this.update$
    .ofType(PostActions.SAVE_POST)
    .map(action => action.payload)
    .switchMap(post => this.svc.savePost(post))
    .map(post => this.postActions.savePOSTSuccess(post));

reducer:

const initialState: PostListState = [];

export default function (state = initialState, action: Action): PostListState {
    switch (action.type) {
        case PostActions.LOAD_POST_SUCCESS: {
            return action.payload;
        }
        case PostActions.SAVE_POST_SUCCESS: {
            console.log('SavePOST SUCCESS',action.payload)
            let index = _.findIndex(state, {_id: action.payload._id});
            if (index >= 0) {
                return [
                    ...state.slice(0, index),
                    action.payload,
                    ...state.slice(index + 1)
                ];
            }
            return state;
        }
        default: {
            return state;
        }
    }
}

in my component i want to subscribe to success callback:

   handlePostUpdated($event) {
     this.post = this.code;
     let _post: Post = Object.assign({}, { _id: this.id, name: this.name, text: this.post });
     this.store.dispatch(this.postActions.savePOST(_post)); //not have "subscribe" method

  }

Thanks for Help

happyZZR1400
  • 2,387
  • 3
  • 25
  • 43

6 Answers6

92

You can subscribe to actions in components as well:

[...]
import { Actions } from '@ngrx/effects';
[...]

@Component(...)
class SomeComponent implements OnDestroy {
    destroyed$ = new Subject<boolean>();

    constructor(updates$: Actions) {
        updates$.pipe(
           ofType(PostActions.SAVE_POST_SUCCESS),
           takeUntil(this.destroyed$)
        )
        .subscribe(() => {
           /* hooray, success, show notification alert etc.. */
        });
    }

    ngOnDestroy() {
        this.destroyed$.next(true);
        this.destroyed$.complete();
    }
}
timray
  • 588
  • 1
  • 9
  • 24
olsn
  • 16,644
  • 6
  • 59
  • 65
  • i can see that there are other ways to do it, but this way worked for me and i feel obliged to mark it as answer – happyZZR1400 Apr 05 '17 at 10:21
  • 2
    There are always many ways to go, but if you just want to show like a small notification, that saving was successful, then this way is imho the "cleanest", since it is a one-time-event and should not go all the way through the store – olsn Apr 05 '17 at 10:26
  • @olsn that is cool approach. Currently just applied in my app ;) – angularrocks.com Apr 05 '17 at 23:34
  • Do I have to save the subscription in constructor to unsubscribe() it in ngOnDestroy for releasing the memory in real world app? Or it will be done automatically by function takeUntil when it receives an event from this.destroyed$? – coderlex Jul 19 '17 at 13:16
  • 4
    the `destroyed$` in combination with `takeUntil(...)` is a pattern where it will automatically unsubscribe so you do not have to keep track of the subscription. – olsn Jul 19 '17 at 13:49
  • Thanks for the answer. I've also noticed that when one adds an event handler to Actions, it repeats the latest action instantly (synchrously). It may lead to duplicating an effect. So instead of more concise `.ofType(SIGNIN_SUCCESS)` I had to write `.skip(1).filter((action: Action) => action.type === SIGNIN_SUCCESS)`. – coderlex Jul 20 '17 at 19:07
  • True, but that's only the case if your components subscribes after the first action - also it depends on what you plan to do, so this is nothing universal that's required for every case. – olsn Jul 20 '17 at 20:20
  • I understand this is possible, but isn't it considered an anti-pattern? I thought components listen to the state changes, not actions. – Michael Oct 11 '17 at 16:49
  • 1
    In general yes, but there is NO project, where it makes sense to go 100% by the book... it just doesn't for so many reasons (time, quality, code-overhead, complexity...ect...), and imho: Alerts are just one of those things - going via a store here would mean that you'd have to implement actions to show the msg, hide the msg, also save the msg to the store, then implement some effect, that will delete it from the store, ect... while it is way more pragmatic to simply call some function from a 3rd party component, that just works - and with using a 3rd you'll save a ton of work. – olsn Oct 11 '17 at 19:17
  • In the end it's about setting your own rules and having a certain consistency - if you want to fanatically/religiously follow some pattern that someone tells you .. fine (i wouldn't advise you to, simply for the fact that there is NO such "golden solution" that fits all cases 100% - but if you still do i guess you'd be fine); if you want to derive your own best practices, also fine (event better if you ask me) - the only thing that i do not think is fine, is if you don't think about it at all and implement it differently at every corner – olsn Oct 11 '17 at 19:22
  • I've found that this works for some cases, but when I need to have the latest state from the store that's being reduced in the reducer for the `DONE` action, this `updates$` observable will fire before the state is updated. Still looking for a solution for that. Maybe `combineLatest` with the selector of that particular state... – TrySpace Feb 23 '18 at 12:01
  • The import statement for the actions is `import { Actions } from '@ngrx/effects';` – Carli Beeli Jun 06 '18 at 08:04
  • I think it's bad idea to handle actions outside reducer/effect/epic. May be it's better to handle SAVE_POST_SUCCESS by setting some ++counter to store. Then you can subscribe to this counter in your component and unsubscribe in OnDestroy method. – Mihail Jan 29 '19 at 14:04
  • For side effects such as displaying a notification this should rightly be an Effect, so the logic put into a Service and called from an effect. Subscribing in this way should be left for things like resetting forms, where it's not practical to do from an effect. – Ian Jul 27 '20 at 13:25
59

Based on this:https://netbasal.com/listening-for-actions-in-ngrx-store-a699206d2210 with some small modifications, as since ngrx 4 there is no Dispatcher anymore, but instead ActionsSubject:

import { ActionsSubject } from '@ngrx/store';
import { ofType } from "@ngrx/effects";

subsc = new Subscription();

constructor(private actionsSubj: ActionsSubject, ....) { 
  this.subsc = this.actionsSubj.pipe(
     ofType('SAVE_POST_SUCCESS')
  ).subscribe(data => {
     // do something...
  });
}

ngOnDestroy() {
  this.subsc.unsubscribe();
}
Dmitry Grinko
  • 13,806
  • 14
  • 62
  • 86
AT82
  • 71,416
  • 24
  • 140
  • 167
  • 4
    Is this any different that doing it with `actions$: Actions` and then doing `.ofType('SAVE_POST_SUCCESS')`? I've noticed that this approach triggers it sooner, and then `ActionSubject`. Does this mean it will trigger only after the reducers have been run? – TrySpace Feb 23 '18 at 12:15
  • the answer of AJT will run before a new state has been initialised, thus will there be a data mismatch, how to prevent this? – Sytham Apr 18 '18 at 16:13
  • 8
    As of Aug 2018, this should be the accepted answer - the accepted answer is deprecated now - use this more modern approach – danday74 Aug 11 '18 at 15:49
  • How to retrieve payload that passed while dispatching action in this approach ? – kandarp Sep 13 '19 at 02:59
  • what does the Subscription object here refer to? – Yehia A.Salam Nov 22 '19 at 20:08
  • 2
    @YehiaA.Salam, it's exactly what it's name, a subscription. If we don't unsubscribe to a subscription, there will be memory leaks, i.e the observable `actionSubject` remains open. – AT82 Nov 22 '19 at 20:17
  • yea but the Subscription object appears to be no longer available in ngrx 8, just tried the code and Subscription can’t be found in any of the ngrx exported objects – Yehia A.Salam Nov 22 '19 at 20:19
  • 3
    It never has been part of it. It is from `rxjs`: `import { Subscription } from 'rxjs'`. Documentation: https://rxjs-dev.firebaseapp.com/guide/subscription :) – AT82 Nov 22 '19 at 20:24
  • There are couple of typo in the above code, since it is less than 10char not able to edit it 1) Subcription -> Subscription , 2) actionsSubject -> actionsSubj – RRR Oct 09 '20 at 05:55
3

you need to have a selector that give you the latest post

this.latestPost$ = this.store.select(fromRoot.getPost);

then you can subscribe to this.latestPost$ observable and redirect to your new route assuming you have declared latestPost$: Observable<Post>; in your component.

for more details see this example app https://github.com/ngrx/example-app

angularrocks.com
  • 26,767
  • 13
  • 87
  • 104
2

It will be better if you also add your reducer function code and show how the saved post affects your state, on savePOSTSuccess action.

In general, you may put an "editing" boolean property in your post state.

On the reducer, you can set a flag value to !editing state by setting the property to false.

Then, you may add a selector on that property and show or hide the form according to this property.

Ran
  • 747
  • 6
  • 8
2

Sure. That's possible. Please consider this example of listening for a saved contact.

export class PageContactComponent implements OnInit, OnDestroy {

  destroy$ = new Subject<boolean>();
    
  constructor(private actionsListener$: ActionsSubject) {}

  ngOnInit() {
    this.actionsListener$
      .pipe(ofType(ContactActionTypes.SAVE_SUCCESS))
      .pipe(takeUntil(this.destroy$))
      .subscribe((data: any) => {
        // Do your stuff here
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Here, I have created an ActionsSubject named actionsListener$. In this example I am listening to ContactActionTypes.SAVE_SUCCESS (an action that happens when a contact has been saved). Describe your code in the subscribe section and don't forget to destroy the subscription.

Edit: And here is what the Action looks like:

export enum ContactActionTypes {
  SAVE_SUCCESS = "[Contact] Save Success",
}

export class ActionContactSaveSuccess implements Action {
  readonly type = ContactActionTypes.SAVE_SUCCESS;

  constructor(readonly payload: { contact: any }) {}
}

export type ContactActions = ActionContactSaveSuccess;
Matt
  • 33,328
  • 25
  • 83
  • 97
0

Angular, Angular... Yet another plugin to be included each time you need something different... :)

But in the given case, seems like it's possible to do that without effects. The same approach as mentioned by @kuncevic-dev

this.store.select(state => state.CompanyLicencesState)
  .pipe(takeUntil(this.destroy))
  .subscribe((state: CompanyLicences.State) => {
  ...
});
this.store.dispatch(new GetCompanyLicences(filter))
  .pipe(
    finalize(() => (this.companyLicencsLoading = false)), 
    takeUntil(this.destroy))
  .subscribe(() => {
  ...
});

Will do the work.

ssuperczynski
  • 3,190
  • 3
  • 44
  • 61
Alexander
  • 1,152
  • 1
  • 16
  • 18