Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
denyo opened this issue May 31, 2019 · 3 comments
Assignees
Milestone

Comments

@denyo
Copy link

denyo commented May 31, 2019

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

@piotrwitek
Copy link
Owner

piotrwitek commented Jun 2, 2019

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.

@issuehunt-oss issuehunt-oss bot added the 💵 Funded on Issuehunt This issue has been funded on Issuehunt label Jun 18, 2019
@issuehunt-oss
Copy link

issuehunt-oss bot commented Jun 18, 2019

@issuehunt has funded $120.00 to this issue.


@piotrwitek piotrwitek pinned this issue Oct 29, 2019
@piotrwitek piotrwitek added this to the v5.3.0 milestone Oct 29, 2019
@piotrwitek piotrwitek changed the title Abstraction of Observable pipe in Epics Add a generic redux-saga factory automatically handling async-actions - createAsyncSaga Dec 24, 2019
@piotrwitek piotrwitek changed the title Add a generic redux-saga factory automatically handling async-actions - createAsyncSaga Add a generic redux-observable factory automatically handling async-actions - createAsyncEpic Dec 24, 2019
@piotrwitek
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants