Skip to content

Add a generic redux-observable factory automatically handling async-actions - createAsyncEpic #162

@denyo

Description

@denyo

Issuehunt badges

Is your feature request related to a real problem or use-case?

While using Epics from redux-observable I find myself writing the same code over and over again. It would be great to have an abstraction of this while being properly typed.

Describe a solution including usage in code example

Let's say you have a regular async action:

export const fetchEmployees = createAsyncAction(
  '@employees/FETCH_EMPLOYEES',
  '@employees/FETCH_EMPLOYEES_SUCCESS',
  '@employees/FETCH_EMPLOYEES_FAILURE',
  '@employees/FETCH_EMPLOYEES_CANCEL'
)<undefined, Employee[], HttpError>();

With the corresponding Epic:

const fetchEmployeesEpic: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { employeeService }) =>
  action$.pipe(
    filter(isActionOf(fetchEmployees.request)),
    switchMap(({ payload }) =>
      employeeService.getEmployees(payload).pipe(
        map(fetchEmployees.success),
        catchErrorAndHandleWithAction(fetchEmployees.failure),
        takeUntilAction(action$, fetchEmployees.cancel)
      )
    )
  );

The two pipeable operators catchErrorAndHandleWithAction and takeUntilAction are defined as:

export const takeUntilAction = <T>(
  action$: Observable<RootAction>,
  action: (payload: HttpError) => RootAction
): OperatorFunction<T, T> => takeUntil(action$.pipe(filter(isActionOf(action))));

export const catchErrorAndHandleWithAction = <T, R>(
  action: (payload: HttpError) => R
): OperatorFunction<T, T | R> =>
  catchError((response) => of(action(response)));

Now the following part inside the Epic's switchMap is pretty much the same in every epic

employeeService.getEmployees(payload).pipe(
    map(fetchEmployees.success),
    catchErrorAndHandleWithAction(fetchEmployees.failure),
    takeUntilAction(action$, fetchEmployees.cancel)
)

The goal is to abstract this in its own operator that might be used like

employeeService.getEmployees(payload).pipe(
    mapUntilCatch(action$, fetchEmployees),
)

As a starting point I already tried

export const mapUntilCatch = <T>(action$: Observable<RootAction>, actions: any) =>
  pipe(
    map(actions.success),
    catchErrorAndHandleWithAction(actions.failure),
    takeUntilAction(action$, actions.cancel)
  );

But I am running into problems with the generics and typing of actions.
I already tried different variations like:

actions: { success: PayloadAC<TypeConstant, T[]>; cancel: never; failure: PayloadAC<TypeConstant, HttpError> }
actions: { success: PayloadAC<TypeConstant, T[]>; cancel: never; failure: PayloadAC<TypeConstant, HttpError> }
actions: ReturnType<AsyncActionBuilder<TypeConstant, TypeConstant, TypeConstant, TypeConstant>>
actions: ActionType<
  AsyncActionCreator<[TypeConstant, any], [TypeConstant, T[]], [TypeConstant, HttpError], [TypeConstant, undefined]>
>

Who does this impact? Who is this for?

People using typescript and redux-observable.

Describe alternatives you've considered (optional)

Using the pipe operator right inside the Epic works:

import { pipe } from 'rxjs';
employeeService.getEmployees(payload).pipe(
    pipe(
        map(fetchEmployees.success),
        catchErrorAndHandleWithAction(fetchEmployees.failure),
        takeUntilAction(action$, fetchEmployees.cancel)
    )
)

The signature of that pipe is the following:

(alias) pipe<Observable<Employee[]>, Observable<PayloadAction<"@employees/FETCH_EMPLOYEES_SUCCESS", Employee[]>>, Observable<PayloadAction<"@employees/FETCH_EMPLOYEES_SUCCESS", Employee[]> | PayloadAction<...>>, Observable<...>>(fn1: UnaryFunction<...>, fn2: UnaryFunction<...>, fn3: UnaryFunction<...>): UnaryFunction<...> (+10 overloads)

Additional context (optional)

In case you need the type of HttpError:

export type HttpError = {
  status?: number;
  error?: string;
  message?: string;
};

IssueHunt Summary

Sponsors (Total: $120.00)

Become a sponsor now!

Or submit a pull request to get the deposits!

Tips

Metadata

Metadata

Assignees

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions