ngrx / platform

Reactive State for Angular
https://ngrx.io
Other
8.04k stars 1.98k forks source link

Simple way of defining actions #723

Closed marcinnajder closed 6 years ago

marcinnajder commented 6 years ago

Hi

We have found a simple way of defining actions in ngrx. We are curious about your opinion.

export type BookActions =
  | { type: 'BOOKS_SEARCH'; payload: string }
  | { type: 'BOOKS_SEARCH_COMPLETE'; payload: Book[] }
  | { type: 'BOOKS_SEARCH_ERROR'; payload: string }
  | { type: 'BOOKS_LOAD'; payload: Book }
  | { type: 'BOOKS_SELECT'; payload: string };

This is all we need to do to define actions for books from your sample app.

Dispatching action looks like this:

  search(query: string) {
    this.store.dispatch<BookActions>({ type: 'BOOKS_SEARCH', payload: query });
  }

Reducer function looks like this:

export function reducer(state = initialState, action: BookActions): State {
  switch (action.type) {
    case 'BOOKS_SEARCH': {
      const query = action.payload;

The implementation of effects may look like this:


@Injectable()
export class AuthEffects {
  @Effect()
  login$ = this.actions$.pipe(
    choose(a => (a.type === 'AUTH_LOGIN' ? a.payload : null)),
    exhaustMap((auth: Authenticate) =>
      this.authService
        .login(auth)
        .pipe(
          map( user => <AuthActions>{ type: 'AUTH_LOGIN_SUCCESS', payload: { user } }),
          catchError(error => of(<AuthActions>{ type: 'AUTH_LOGIN_FAILURE', payload: error })
          )
        )
    )
  );

  @Effect({ dispatch: false })
  loginSuccess$ = this.actions$.pipe(
    filter(a => a.type === 'AUTH_LOGIN_SUCCESS'),
    tap(() => this.router.navigate(['/']))
  );

  @Effect({ dispatch: false })
  loginRedirect$ = this.actions$.pipe(
    filter(a => a.type === 'AUTH_REDIRECT' || a.type === 'AUTH_LOGOUT'),
    tap(authed => {
      this.router.navigate(['/login']);
    })
  );

  constructor(
    private actions$: Actions<AuthActions>,
    private authService: AuthService,
    private router: Router
  ) {}
}

Where choose is a tiny utility function:

export function choose<T extends Action, R>(selector: (a: T) => R | null ): OperatorFunction<T, R> {
  return o =>
    o.pipe(
      map(a => selector(a)),
      filter(a => (a as any) as boolean)
    ) as Observable<R>;
}

Here you can find all changes we need to make to introduce this approach to sample ngrx app

https://github.com/marcinnajder/platform/commit/838185ff53819e1a01057c4f06880368f1adb848

This way we don't have to define classes and enums for actions. We still have type safety in all places and the VS Code intellisense works correctly.

kind regards, marcin

sandangel commented 6 years ago

you can take a look at my project https://github.com/sandangel/ngrx-utils using ofAction operator. Much cleaner way I think. ^^

brandonroberts commented 6 years ago

@marcinnajder if that's the way you want to define actions, that's fine. You lose some ability to easily refactor your action types. Thanks for the example!

marcinnajder commented 6 years ago

Thanks for comments and links!

@sandangel interesting idea with generating separate file ... action.generated.ts

I saw ngrx-actions by @amcdnl and great interview and many very good presentations from @vsavkin. Different approaches but still a lot of code to read.

Actions are very similar to discriminated unions in F#.

type BookActions = 
  | Search of string
  | SearchComplete of Book[]
  | SearchError of string
  | Load of Book
  | Select of string

Just look how expressive this code is. This was an inspiration for my solution. The readability is the key feature but still we have type safety, editor support, no JS code after compilation, no tool or runtime lib required.

I recommend you this video Domain Modeling Made Functional - Scott Wlaschin. It can change how you think about code :)

kind regards, marcin

nhhockeyplayer commented 6 years ago

Do these solutions resolve the deprecated payload impact?

all my actions are useless now since I upgraded and I need to refactor

I am not a fan of the presented way because of havin to spell out the text of action in a string when it should be nailed with an enum... leaves room for text string code errors

marcinnajder commented 6 years ago

How exactly the definition of action is changed ? (the specific action is removed, the whole actions type (tagged union type) is removed, the shape of payload is changed, the name of action type is changed, ... )

and how often such changes are made ?

Thanks for comment