Search code examples
reactjstypescriptrxjsredux-observable

How to emit multiple actions in one epic using redux-observable?


I'm new to rxjs/redux observable and want to do two things:

1) improve this epic to be more idiomatic

2) dispatch two actions from a single epic

Most of the examples I see assume that the api library will throw an exception when a fetch fails, but i've designed mine to be a bit more predictable, and with Typescript union types I'm forced to check an ok: boolean value before I can unpack the results, so understanding how to do this in rxjs has been a bit more challenging.

What's the best way to improve the following? If the request is successful, I'd like to emit both a success action (meaning the user is authorized) and also a 'fetch account' action, which is a separate action because there may be times where I need to fetch the account outside of just 'logging in'. Any help would be greatly appreciated!

const authorizeEpic: Epic<ActionTypes, ActionTypes, RootState> = action$ =>
  action$.pipe(
    filter(isActionOf(actions.attemptLogin.request)),
    switchMap(async val => {
      if (!val.payload) {
        try {
          const token: Auth.Token = JSON.parse(
            localStorage.getItem(LOCAL_STORAGE_KEY) || ""
          );
          if (!token) {
            throw new Error();
          }
          return actions.attemptLogin.success({
            token
          });
        } catch (e) {
          return actions.attemptLogin.failure({
            error: {
              title: "Unable to decode JWT"
            }
          });
        }
      }

      const resp = await Auth.passwordGrant(
        {
          email: val.payload.email,
          password: val.payload.password,
          totp_passcode: ""
        },
        {
          url: "localhost:8088",
          noVersion: true,
          useHttp: true
        }
      );

      if (resp.ok) {
        return [
          actions.attemptLogin.success({
            token: resp.value
          })
          // EMIT action 2, etc...
        ];
      }

      return actions.attemptLogin.failure(resp);
    })
  );

Solution

  • The docs for switchMap indicate the project function (the lambda in your example) may return the following:

    type ObservableInput<T> = SubscribableOrPromise<T> | ArrayLike<T> | Iterable<T>
    

    When a Promise<T> is returned, the resolved value is simply emitted. In your example, if you return an array from your async scope, the array will be sent directly to the Redux store. Assuming you have no special Redux middlewares setup to handle an array of events, this is likely not what you want. Instead, I would recommend returning an observable in the project function. It's a slight modification to your example:

    const authorizeEpic: Epic<ActionTypes, ActionTypes, RootState> = action$ =>
      action$.pipe(
        filter(isActionOf(actions.attemptLogin.request)), // or `ofType` from 'redux-observable'?
        switchMap(action => {
          if (action.payload) {
            try {
              const token: Auth.Token = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || "")
              if (!token) {
                throw new Error()
              }
    
              // return an observable that emits a single action...
              return of(actions.attemptLogin.success({
                token
              }))
            } catch (e) {
              // return an observable that emits a single action...
              return of(actions.attemptLogin.failure({
                error: {
                  title: "Unable to decode JWT"
                }
              }))
            }
          }
    
          // return an observable that eventually emits one or more actions...
          return from(Auth.passwordGrant(
            {
              email: val.payload.email,
              password: val.payload.password,
              totp_passcode: ""
            },
            {
              url: "localhost:8088",
              noVersion: true,
              useHttp: true
            }
          )).pipe(
            mergeMap(response => response.ok
              ? of(
                actions.attemptLogin.success({ token: resp.value }),
                // action 2, etc...
              )
              : of(actions.attemptLogin.failure(resp))
            ),
          )
        }),
      )
    

    I don't have your TypeScript type definitions, so I can't verify the example above works exactly. However, I've had quite good success with the more recent versions of TypeScript, RxJS, and redux-observable. Nothing stands out in the above that makes me think you should encounter any issues.