reasync-hooks
v0.1.1
Published
A library to help developer keep track of redux async action states.
Downloads
1
Readme
Reasync-hooks
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: "[email protected]" } });
}, 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: "[email protected]" } });
}, 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: "[email protected]" } });
}, 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 actionTypes
changes 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
- [x] Add test
License
[MIT]