reasync-hooks
TypeScript icon, indicating that this package has built-in type declarations

0.1.1 • Public • Published

Reasync-hooks

GitHub license npm version CircleCI

A library to keep track of redux async action states, based on react-redux and react hooks.

Table of Contents

Installation

React Redux Async Hooks requires React 16.8.3 and React-redux 7.10 or later.

npm install --save reasync-hooks

This assumes that you’re using npm package manager with a module bundler like Webpack or Browserify to consume CommonJS modules.

Example

Basis example

You can play around with the following example in this codesandbox:

Step 1

Create store
import { applyMiddleware, combineReducers, createStore, compose } from "redux";
import { asyncReduxMiddlewareCreator , asyncStateReducer } from "reasync-hooks";
 
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const asyncReduxMiddleware = asyncReduxMiddlewareCreator();
const rootReducer = combineReducers({
  asyncState: asyncStateReducer
});
const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(asyncReduxMiddleware))
);

Step 2

Create async actions
import { asyncActionCreator } from "reasync-hooks";
 
const FULFILLED_ACTION = "FULFILLED_ACTION";
const REJECTED_ACTION = "REJECTED_ACTION";
const asyncFulfilledAction = asyncActionCreator(
  FULFILLED_ACTION,
  //Return a fulfilled Promise
  () =>
    new Promise(function(resolve) {
      setTimeout(function() {
        resolve("");
      }, 1000);
    })
);
const asyncRejectedAction = asyncActionCreator(
  REJECTED_ACTION,
  //Return a rejected Promise
  () =>
    new Promise(function(resolve, reject) {
      setTimeout(function() {
        reject("");
      }, 1000);
    })
);

Step 3

Use hooks in your component
import {
  useIsAsyncPendingSelector,
  useOnAsyncFulfilled,
  useOnAsyncRejected
} from "reasync-hooks";
import { useDispatch } from "react-redux";
import { Button, message } from "antd";
 
const BasisExample = () => {
  const dispatch = useDispatch();
  const isFulfilledActionPending = useIsAsyncPendingSelector([
    FULFILLED_ACTION
  ]);
  const isRejectedActionPending = useIsAsyncPendingSelector([REJECTED_ACTION]);
  //Notify something when async action is from pending to fulfilled
  useOnAsyncFulfilled([FULFILLED_ACTION], asyncType => {
    message.success(asyncType);
  });
  //Notify something when async action is from pending to rejected
  useOnAsyncRejected([REJECTED_ACTION], asyncType => {
    message.error(asyncType);
  });
  return (
    <div className="App">
      <Button
        onClick={() => dispatch(asyncFulfilledAction)}
        loading={isFulfilledActionPending}
        type="primary"
      >
        asyncFulfilledAction
      </Button>
      <Button
        onClick={() => dispatch(asyncRejectedAction)}
        loading={isRejectedActionPending}
        type="danger"
      >
        asyncRejectedAction
      </Button>
    </div>
  );
};

Step 4

Nest the component inside of a <Provider>
import { Provider } from "react-redux";
 
const App = () => (
  <Provider store={store}>
    <BasisExample />
  </Provider>
);
export default App;

Complete example:

import React from "react";
import { applyMiddleware, combineReducers, createStore, compose } from "redux";
import {
  asyncReduxMiddlewareCreator,
  asyncStateReducer,
  useIsAsyncPendingSelector,
  useOnAsyncFulfilled,
  useOnAsyncRejected,
  asyncActionCreator
} from "reasync-hooks";
import { Provider, useDispatch } from "react-redux";
import { Button, message } from "antd";
import("antd/dist/antd.css");
import ("./App.css");
 
/*
Step 1: create store
 */
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const asyncReduxMiddleware = asyncReduxMiddlewareCreator();
const rootReducer = combineReducers({
  asyncState: asyncStateReducer
});
const store = createStore(
    rootReducer,
    composeEnhancers(applyMiddleware(asyncReduxMiddleware))
);
 
/*
Step 2: create async actions
 */
const FULFILLED_ACTION = "FULFILLED_ACTION";
const REJECTED_ACTION = "REJECTED_ACTION";
const asyncFulfilledAction = asyncActionCreator(
    FULFILLED_ACTION,
    //Return a fulfilled Promise
    () =>
        new Promise(function(resolve) {
          setTimeout(function() {
            resolve("");
          }, 1000);
        })
);
const asyncRejectedAction = asyncActionCreator(
    REJECTED_ACTION,
    //Return a rejected Promise
    () =>
        new Promise(function(resolve, reject) {
          setTimeout(function() {
            reject("");
          }, 1000);
        })
);
 
/*
Step 3: use hooks in your component
 */
const BasisExample = () => {
  const dispatch = useDispatch();
  const isFulfilledActionPending = useIsAsyncPendingSelector([
    FULFILLED_ACTION
  ]);
  const isRejectedActionPending = useIsAsyncPendingSelector([REJECTED_ACTION]);
  //Notify something when async action is from pending to fulfilled
  useOnAsyncFulfilled([FULFILLED_ACTION], asyncType => {
    message.success(asyncType);
  });
  //Notify something when async action is from pending to rejected
  useOnAsyncRejected([REJECTED_ACTION], asyncType => {
    message.error(asyncType);
  });
  return (
      <div className="App">
        <Button
            onClick={() => dispatch(asyncFulfilledAction)}
            loading={isFulfilledActionPending}
            type="primary"
        >
          asyncFulfilledAction
        </Button>
        <Button
            onClick={() => dispatch(asyncRejectedAction)}
            loading={isRejectedActionPending}
            type="danger"
        >
          asyncRejectedAction
        </Button>
      </div>
  );
};
 
/*
Step4: nest the component inside of a `<Provider>`
 */
const App = () => (
    <Provider store={store}>
      <BasisExample />
    </Provider>
);
export default App;

Advanced example: fetch data and handle error

There are two examples. One uses the reasync-hooks, the other only uses hooks. Both examples implement the same goal that fetch data and, if successful, notify the data, otherwise notify the error message.

1.Only use react hooks

Note: This example is only used to help users understand the usage of reasync-hooks and is not recommended.

import React, { useEffect, useState } from "react";
import {Button,message} from 'antd'
 
const ExampleOnlyUseHooks = () => {
  //Mock that fetch data successfully
  const fetchDataSuccess = () =>
      new Promise(function(resolve) {
        setTimeout(function() {
          //Receive data
          resolve({ profile: { email: "someone@email.com" } });
        }, 1000);
      });
 
  //Mock that an error occurs when fetch data
  const fetchDataError = () =>
      new Promise(function(resolve, reject) {
        setTimeout(function() {
          //An error occurs
          reject({ msg: "something wrong" });
        }, 1000);
      });
 
  const [isFulfilledAsyncPending, setIsFulfilledAsyncPending] = useState(false);
  const [data, setSetData] = useState();
 
  const [isRejectedAsyncPending, setIsRejectedAsyncPending] = useState(false);
  const [error, setError] = useState();
 
  useEffect(() => {
    if (data) message.success(data.profile.email);
  }, [data]);
 
  useEffect(() => {
    if (error) message.error(error.msg);
  }, [error]);
 
  return (
      <div className="App">
        <Button
            onClick={() => {
              setIsFulfilledAsyncPending(true);
              fetchDataSuccess().then(data => {
                setIsFulfilledAsyncPending(false);
                setSetData(data);
              });
            }}
            loading={isFulfilledAsyncPending}
            type='primary'
        >
          asyncFulfilledAction
        </Button>
        <Button
            onClick={() => {
              setIsRejectedAsyncPending(true);
              fetchDataError().catch(error => {
                setIsRejectedAsyncPending(false);
                setError(error);
              });
            }}
            loading={isRejectedAsyncPending}
            type='danger'
        >
          asyncRejectedAction
        </Button>
      </div>
  );
};

2.Use reasync-hooks

You can play around with the following example in this codesandbox:

Step 1

Customize the redux middleware
import { asyncReduxMiddlewareCreator , asyncStateReducer } from "reasync-hooks";
 
const fulfilledHandler = (resolveValue, action, dispatch) => {
  dispatch({ ...action, data: resolveValue });
};
const rejectedHandler = (rejectedReason, action, dispatch) => {
  dispatch({
    ...action,
    error: rejectedReason
  });
};
const asyncReduxMiddleware = asyncReduxMiddlewareCreator(
  fulfilledHandler,
  rejectedHandler
);

Step 2

Create async actions
import { asyncStateReducer, asyncActionCreator } from "reasync-hooks";
 
const FULFILLED_ACTION = "FULFILLED_ACTION";
const REJECTED_ACTION = "REJECTED_ACTION";
//Mock that fetch data successfully
const fetchDataSuccess = () =>
  new Promise(function(resolve) {
    setTimeout(function() {
      //Receive data
      resolve({ profile: { email: "someone@email.com" } });
    }, 1000);
  });
//Mock that an error occurs when fetch data
const fetchDataError = () =>
  new Promise(function(resolve, reject) {
    setTimeout(function() {
      //An error occurs
      reject({ msg: "something wrong" });
    }, 1000);
  });
const asyncFulfilledAction = asyncActionCreator(
  FULFILLED_ACTION,
  fetchDataSuccess
);
const asyncRejectedAction = asyncActionCreator(REJECTED_ACTION, fetchDataError);

Step 3

Create reducers

Note: To reduce boilerplate, you can use createReducer.

import {
  fulfilledTypeCreator,
  rejectedTypeCreator
} from "reasync-hooks";
 
const fulfilledReducer = (state = {}, action) => {
  if (action.type === fulfilledTypeCreator(FULFILLED_ACTION)) {
    return {
      ...state,
      ...action.data
    };
  }
  return state;
};
const errorReducer = (state = {}, action) => {
  if (action.type === rejectedTypeCreator(REJECTED_ACTION))
    return {
      ...state,
      [REJECTED_ACTION]: action.error
    };
  return state;
};

Step 4

Create store
import { applyMiddleware, combineReducers, createStore, compose } from "redux";
import { asyncReduxMiddlewareCreator , asyncStateReducer } from "reasync-hooks";
 
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers({
  user: fulfilledReducer,
  error: errorReducer,
  asyncState: asyncStateReducer
});
export const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(asyncReduxMiddleware))
);

Step 5

Use hooks in your component
import {
  useIsAsyncPendingSelector,
  useOnAsyncFulfilled,
  useOnAsyncRejected
} from "reasync-hooks";
import { useDispatch, useStore } from "react-redux";
import { Button, message } from "antd";
 
const AdvancedExample = () => {
  const dispatch = useDispatch();
  const store = useStore();
  const isFulfilledActionPending = useIsAsyncPendingSelector([
    FULFILLED_ACTION
  ]);
  const isRejectedActionPending = useIsAsyncPendingSelector([REJECTED_ACTION]);
  //Notify data when async action changes from pending to fulfilled
  useOnAsyncFulfilled([FULFILLED_ACTION], () => {
    message.success(store.getState().user.profile.email);
  });
  //Notify error message when async action is from pending to rejected
  useOnAsyncRejected([REJECTED_ACTION], asyncType => {
    message.error(store.getState().error[asyncType].msg);
  });
  return (
    <div className="App">
      <Button
        onClick={() => dispatch(asyncFulfilledAction)}
        loading={isFulfilledActionPending}
        type="primary"
      >
        asyncFulfilledAction
      </Button>
      <Button
        onClick={() => dispatch(asyncRejectedAction)}
        loading={isRejectedActionPending}
        type="danger"
      >
        asyncRejectedAction
      </Button>
    </div>
  );
};

Step 6

Nest the component inside of a <Provider>
import { Provider } from "react-redux";
 
const App = () => (
  <Provider store={store}>
    <AdvancedExample />
  </Provider>
);
export default App;

complete example:

import React from "react";
import { applyMiddleware, combineReducers, createStore, compose } from "redux";
import {
  fulfilledTypeCreator,
  rejectedTypeCreator,
  asyncReduxMiddlewareCreator,
  asyncStateReducer,
  useIsAsyncPendingSelector,
  useOnAsyncFulfilled,
  useOnAsyncRejected,
  asyncActionCreator
} from "reasync-hooks";
import { Provider, useDispatch, useStore } from "react-redux";
import { Button, message } from "antd";
import("antd/dist/antd.css");
import("./App.css");
 
/*
Step 1: customize the redux  middleware
 */
const fulfilledHandler = (resolveValue, action, dispatch) => {
  dispatch({ ...action, data: resolveValue });
};
const rejectedHandler = (rejectedReason, action, dispatch) => {
  dispatch({
    ...action,
    error: rejectedReason
  });
};
const asyncReduxMiddleware = asyncReduxMiddlewareCreator(
    fulfilledHandler,
    rejectedHandler
);
 
/*
Step 2: create async actions
*/
const FULFILLED_ACTION = "FULFILLED_ACTION";
const REJECTED_ACTION = "REJECTED_ACTION";
//Mock that fetch data successfully
const fetchDataSuccess = () =>
    new Promise(function(resolve) {
      setTimeout(function() {
        //Receive data
        resolve({ profile: { email: "someone@email.com" } });
      }, 1000);
    });
//Mock that an error occurs when fetch data
const fetchDataError = () =>
    new Promise(function(resolve, reject) {
      setTimeout(function() {
        //An error occurs
        reject({ msg: "something wrong" });
      }, 1000);
    });
const asyncFulfilledAction = asyncActionCreator(
    FULFILLED_ACTION,
    fetchDataSuccess
);
const asyncRejectedAction = asyncActionCreator(REJECTED_ACTION, fetchDataError);
 
/*
Step 3: create reducers
 */
const fulfilledReducer = (state = {}, action) => {
  if (action.type === fulfilledTypeCreator(FULFILLED_ACTION)) {
    return {
      ...state,
      ...action.data
    };
  }
  return state;
};
const errorReducer = (state = {}, action) => {
  if (action.type === rejectedTypeCreator(REJECTED_ACTION))
    return {
      ...state,
      [REJECTED_ACTION]: action.error
    };
  return state;
};
 
/*
 Step 4: create store
 */
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers({
  user: fulfilledReducer,
  error: errorReducer,
  asyncState: asyncStateReducer
});
export const store = createStore(
    rootReducer,
    composeEnhancers(applyMiddleware(asyncReduxMiddleware))
);
 
/*
Step 5: use hooks in your component
 */
const AdvancedExample = () => {
  const dispatch = useDispatch();
  const store = useStore();
  const isFulfilledActionPending = useIsAsyncPendingSelector([
    FULFILLED_ACTION
  ]);
  const isRejectedActionPending = useIsAsyncPendingSelector([REJECTED_ACTION]);
  //Notify data when async action changes from pending to fulfilled
  useOnAsyncFulfilled([FULFILLED_ACTION], () => {
    message.success(store.getState().user.profile.email);
  });
  //Notify error message when async action is from pending to rejected
  useOnAsyncRejected([REJECTED_ACTION], asyncType => {
    message.error(store.getState().error[asyncType].msg);
  });
  return (
      <div className="App">
        <Button
            onClick={() => dispatch(asyncFulfilledAction)}
            loading={isFulfilledActionPending}
            type="primary"
        >
          asyncFulfilledAction
        </Butto
        <Button
            onClick={() => dispatch(asyncRejectedAction)}
            loading={isRejectedActionPending}
            type="danger"
        >
          asyncRejectedAction
        </Button>
      </div>
  );
};
 
/*
Step 6: nest the component inside of a `<Provider>
 */
const App = () => (
    <Provider store={store}>
      <AdvancedExample />
    </Provider>
);
export default App;

API

asyncActionCreator

const asyncAction = () => actionTypeCreator(type, asyncAction, extraArgument)

Parameters

actionType:string : An async action type.

asyncFunction:(getState)=>Promise: A function that executes an asynchronous operation.

extraArgument?:any : A custom argument that will be available by action.extraArgument in the async action workflow.

Return

asyncAction:{types:[pendingType,fulfilledType,rejectedType],asyncFunction,extraArgument}

The reduxMiddleware that is created by asyncReduxMiddlewareCreator will only response the action with a types property . In fact, the idea behind react-redux-async-hooks is that dispatch a corresponding action(pendingType, fulfilledType, rejectedType) when the Promise that asyncFunction returns is in a different state(pending,fulfilled,rejected).

Note: asyncFunction must be a function that returns a Promise.

asyncReduxMiddlewareCreator

const asyncReduxMiddleware = asyncReduxMiddleware(fullfilledHandler, rejectedHandler)

Parameters

fullfilledHandler?:(resolveValue, action, dispatch, getState) => void: If the promise has already been fulfilled, this handler will be called.

rejectedHandler?:( rejectedReason, action, dispatch, getState) => void: If the promise has already been rejected, this handler will be called.

Note: Default handlers only call diapatch(action).

Return

void

Customize your asyncrReduxMiddeware.

asyncStateReducer

A reducer that specifies how the application's state changes in response to async action to the store.

useIsAsyncPendingSelector

const isPending = useIsAsyncPendingSelector(actionTypes, asyncStateReducerKey)

Parameters

actionTypes:string[]: A group of async actions that are kept track of.

asyncStateReducerKey:string="asyncState" : Under the hood, useIsAsyncPendingSelector tries to get async action states by

//https://react-redux.js.org/api/hooks#useselector
useSelector(state => state[asyncStateReducerKey]);

So you have to ensure asyncStateReducerKey same with the key that is passed to combinReducers for asyncSateReducer .

Return

isAsyncPending:boolen: True means that at least one among asyncTypes is in pending . False means that all in asyncTypes are in fulfilled or rejected.

useOnAsyncFulfilled

useOnAsyncFulfilled(actionTypes, handler, asyncStateReducerKey)

Parameters

actionTypes:string[]: A group of async actions that are kept track of.

handler:(asyncType)=>void: Run when any one of actionTypes changes from pending to fulfilled. The asyncType is passed to handler is the one that triggers the handler.

asyncStateReducerKey:string="asyncState" : Same with this parameter in useIsAsyncPendingSelector.

Return

void

useOnAsyncRejected

useOnAsyncRejected(actionTypes, handler, asyncStateReducerKey)

Parameters

actionTypes:string[]: A group of async action types that are kept track of.

handler:(actionType)=>void: Run when one of actionTypeschanges from pending to rejected. The actionType is passed to handler is the one that triggers the handler.

asyncStateReducerKey="asyncState": Same with this parameter in useIsAsyncPendingSelector.

Return

void

fulfilledTypeCreator

const fulfilledType = fulfilledTypeCreator(actionType)

Parameters

actionType:string: An action type that represents an async action.

Return

asyncFulfilledType:string: An async action type that you can use in your reducers to catch up the async action when it is in fulfilled.

rejectedTypeCreator

const rejectedType = rejectedTypeCreator(actionType)

Parameters

actionType:string: An action type that represents an async action.

Return

asyncFulfilledType:string: An async action type that you can use in your reducers to catch up the async action when it is in rejected.

Todo

  • Add test

License

[MIT]

Readme

Keywords

Package Sidebar

Install

npm i reasync-hooks

Weekly Downloads

0

Version

0.1.1

License

MIT

Unpacked Size

66.5 kB

Total Files

42

Last publish

Collaborators

  • kevin0225