More @ngrx/effects

678 views Asked by At

I asked a question a while ago surrounding the @ngrx/effects library but the responses do not resolve my issue entirely.

I am now happy that I understand how actions and effects relate to each other, but I am still unsure as to what functions they perform. The example app goes some way to helping me, but it seems that the effects are doing all the grunt work: the loadCollection$ effect, for example, appears to be loading all of the resources; and a subsequent dispatch of an AddBookSuccessAction appears to add the book to the collection.

In this app, the AddBookAction appears to do nothing in the reducer, but the effect is loading the resource and dispatching a success action.

I guess really what I want to know is about separation of concerns: what should actions be doing, and what should effects be doing?

1

There are 1 answers

4
Paul Samsotha On BEST ANSWER

When you introduce @ngrx/effects into your application, you need to think a little bit differently. Take for example a login component and a user state

@Component({})
class LoginComponent {

  login(form: any) { // implementation }
}

@Component({})
class AppComponent {
  user: Observable<User>;

  constructor(store: Store<AppState>) {
    this.user = store.select('user');
  }
}

Without using effects, your login implementation might look something like

login(form: any) {
  this.authService.login(new Credentials(form))
    .map(res => res.json() as User)
    .subscribe(user => {
      this.store.dispatch({ type: UPDATE_USER, payload: user });
    });
}

When the dispatch is called, the action is used by the reducer to update the user state. So the action doesn't really do anything. The reducer can use the payload to update the state, based on the action. The subscribing (in this case) AppComponent will get the updated user.

When you introduce effects, the main difference is how the login is handle. Nothing changes about the USER_UPDATED action or the user reducer. What changes is that you introduce another action, that is dispatched by the login method. So now your new login is simply

login(form: any) {
  this.store.dispatch({ type: LOGIN, payload: form })
}

Now the purpose of the effect, is to handle what would normally have been handled in the login. It will listen for the LOGIN action, and then transform itself into the USER_UPDATED action.

@Injectable()
class LoginEffects {
  constructor(private actions: Actions, private authService: AuthService) {}

  @Effect
  login$ = this.actions
    .ofType(LOGIN)
    .map(action => new Credentials(action.payload))
    .switchMap(credentials => {
      return this.authService.login(credentials)
        .map(res => res.json() as User)
        .map({ type: USER_UPDATED, payload: user })
    });
}

You can see that from the LOGIN action, we get the credentials, and use switchMap to return different Observable, one that contains the new (UPDATED_USER) Action. So it's just a transformation. No one has to call the effect. This is automatically subscribed to.

The benefit of doing it this way, is that it makes your component simpler, and easier to test. Also puts all the side effects into one place, which makes it easier to reason about and test. All your components do is subscribe to state and dispatch actions. The effect will handle all the legwork and dispatch the appropriate resulting actions.

As far as the "Action Classes", these are not mandatory. An Action is nothing more than an object with two properties type and payload that get passed to the reducer. The purpose of having action classes is to act as action creators. For example

class UserActions {
  static readonly USER_UPDATED = 'USER_UPDATE';

  userUpdated(user: User): Action {
    return {
      type: UserActions.USER_UPDATED,
      payload: user
    }
  }
}

Now instead of creating the actions yourself, you just call the appropriate method to get the action

constructor(private userActions: UserActions) {}

@Effect
login$ = this.actions
  .ofType(LOGIN)
  .map(form => new Credentials(form))
  .switchMap(credentials => {
    return this.authService.login(credentials)
      .map(res => res.json() as User)
      .map(this.userActions.updateUser(user))
  });

You can see the last map call where we create the Action just be calling the userActions.updateUser.

There are a few benefits to doing this. For one, you get strong typing. When you create the action yourself, there's a chance you might pass the wrong payload. But when you have a method for which you know what the argument type needs to be, you have less chance of a mistake.

Also when you use the action creators, you can accept different arguments. It doesn't have to be the payload. This way you can create the payload yourself (and may introduce some logic) in a reproducible and testable way.

See Also: