2.0.4 • Published 4 years ago

redux-async-epic v2.0.4

Weekly downloads
23
License
Apache-2.0
Repository
github
Last release
4 years ago

Make async actions easy.

Redux-Observable is the great library based on Rxjs and helps you to handle side-effects of Redux managable application.

To reduce the boilerplate of handling async actions, I've created the redux-async-epic. It includes async-epic itself and gives you some handy helpers to manage outgoing actions.

Travis (.org) codecov Commitizen friendly semantic-release

Instalation

To install the stable version:

npm install --save redux-async-epic

This assumes you are using npm as your package manager.

Dependencies

The library depends only on rxjs but also has 2 peer dependencies:

  • redux-observable
  • redux

Why do I need it?

Usually async actions includes 3 major steps:

  • request (turn on pending status)
  • wait for response
  • response (turn of pending status)

That's why you have to keep minimum three actions:

  • {type: "fetch"}
  • {type: "success"}
  • {type: "failure"}

if you want to abort the process you will need a fourth action

  • {type: "abort"}

So, each time when you working with it using redux-observable you have to make something like this:

// epic.js

import { types, fullfill, failure } from "./actions";

const epic = (action$) => (
  action$.pipe(
    ofType(types.fetch),
    mergeMap(action =>
      ajax(action.payload).pipe(
        map(success),
        takeUntil(action$.pipe(
          ofType(types.abort)
        ))
        catchError(failure)
      )
    )
  )
)

A boilerplate action creators.

// actions.js

export const types = {
  fetch: "fetch",
  success: "success",
  failure: "failure",
  abort: "abort"
};

export const fetch = payload => ({
  type: types.fetch,
  payload
});

export const success = payload => ({
  type: types.success,
  payload
});

export const failure = error => ({
  type: types.failure,
  error
});

export const abort = () => ({
  type: types.abort
});

And finally the reducer:

// reducer.js

import { types } from "./actions";

const initialState = { uiStatus: "idle", items: [], error: null };

export default (state = initialState, action) => {
  switch (action.type) {
    case types.fetch: {
      return {
        ...state,
        uiStatus: "pending"
      };
    }

    case types.success: {
      return {
        ...state,
        uiStatus: "success",
        items: action.payload
      };
    }

    case types.failure: {
      return {
        ...state,
        uiStatus: "failure",
        error: action.error
      };
    }

    case types.abort: {
      return {
        ...state,
        uiStatus: "idle"
      };
    }

    default: {
      return state;
    }
  }
};

There is lot of boilerplate code, isn't it?

Could it be better?

It's time to redux-async-epic to shine! To use it, you need to combine async epic to your root epic.

// root-epic.js

import { combineEpics } from "redux-observable";
import { asyncEpic } from "redux-async-epic";

export const rootEpit = combineEpics(asyncEpic); // more of your epics

Now you should define your async action as flux-standard-action:

// actions.js

import { async, getAbortType } from "redux-async-epic";

export const types = {
  fetch: "fetch"
};

export const fetch = payload => ({
  type: types.fetch,
  payload,
  meta: {
    [async]: true,
    method: ({ payload }) => ajax(payload)
  }
});

export const abort = () => ({
  type: getAbortType(types.fetch) // generates: fetch/abort
});

That's it!

  • You don't need to create more actions to handle response or pending status
  • You don't need to create epic that handles the fetch action

And finally the reducer:

// reducer.js
import { getSuccessType, getFailureType, getAbortType } from "redux-async-epic";

// you can reduce a boilerpalte even more by using "redux-actions"
import { handleActions } from "redux-actions";
import { types } from "./actions";

const initialState = { uiStatus: "idle", items: [], error: null };

export default handleActions({
  [types.fetch]: state => {
    return {
      ...state,
      uiStatus: "pending"
    };
  },

  [getSuccessType(types.fetch)]: (state, action) => {
    return {
      ...state,
      uiStatus: "success",
      items: action.payload
    };
  },

  [getFailureType(types.fetch)]: (state, action) => {
    return {
      ...state,
      uiStatus: "failure",
      error: action.payload
    };
  },

  [getAbortType]: state => {
    return {
      ...state,
      uiStatus: "idle"
    };
  }
});

How it works?

When epic gets an async action it generates three more actions:

  • {type: "fetch"}
  • {type: "fetch/pending", payload: true}
  • {type: "fetch/success"} or {type: "fetch/failure"}
  • {type: "fetch/pending", payload: false}

When async action is pending it also is listening the {type: "fetch/abort"} action, to break the execution.

The benefits

Showing background process

As you can see, each time when async action fires it follows by 2 pending actions. That's why you can listen for it and make a global UI spinner.

Global error handler

When async action is failed redux-async-epic generates a special failure action which looks like that:

{
  type: "async-action/failure",
  error: "something went wront",
  meta: [
    [failure]: true,
    originalPayload: {
      user: "admin"
    }
  ]
}

Where failure is a special unique symbol. Here the example how you can make a global error handler:

// global failure epic
import { isFailureAction } from "redux-async-epic";
import { filter, tap, ignoreElements } from "rxjs/operators";

// just for example
import { clearCredentials } from "./session/lib";
import { logUnauthorizedAccess } from "./analytics";
import { redirectToErrorPage } from "./router";

export default action$ =>
  action.pipe(
    filter(isFailureAction),
    filter(({ error }) => error.status === 403),
    tap(action => {
      clearCredentials();
      logUnauthorizedAccess(action);
      redirectToErrorPage();
    }),
    ignoreElements()
  );

Fire another action on succes

In this example we are requiring first 10 comments, and when request is fulfilled we fire another request to get next page of comments.

// helper
const commentRequest = ({ payload }) =>
  ajax({
    url: "https://comments.server.com",
    method: "post",
    body: payload
  });

// primary action
const fetchComments = (
  payload = {
    offset: 0,
    limit: 10
  }
) => ({
  type: "fetch",
  payload,
  meta: {
    [async]: true,
    method: commentRequest,
    onSuccess: () => prefetchNextPage(payload)
  }
});

// on-success action
const prefetchNextPage = payload => {
  const config = {
    ...payload,
    offset: payload.offset + payload.limit
  };

  return {
    type: "prefetch",
    payload: config,
    meta: {
      [async]: true,
      method: commentRequest
    }
  };
};

You can fire even more actions by passing an array of them:

onSuccess: () => [first(), second(), third()];
2.0.4

4 years ago

2.0.3

5 years ago

2.0.2

5 years ago

2.0.1

5 years ago

2.0.0

6 years ago

1.1.0

6 years ago

1.0.0

6 years ago