typesafe-actions icon indicating copy to clipboard operation
typesafe-actions copied to clipboard

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

Open denyo opened this issue 6 years ago • 3 comments

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

denyo avatar May 31 '19 09:05 denyo

I think the root issue is in optional cancel property in async action type, which is a conditional type and is not resolving correctly when used on generics with higher-order function in rxjs pipe operator type.

I think I might be able to fix that by removing the conditional type from cancel property. I'll try tomorrow.

piotrwitek avatar Jun 02 '19 23:06 piotrwitek

@issuehunt has funded $120.00 to this issue.


issuehunt-oss[bot] avatar Jun 18 '19 04:06 issuehunt-oss[bot]

I'm thinking to add a more generic abstraction that would handle the most common epic creation using createAsyncEpic helper function that would cover the handling of success, error and optional cancel automatically.

piotrwitek avatar Dec 24 '19 00:12 piotrwitek