A Redux middleware extracting the asynchronous behavior of sending API requests.
- Create the middleware and put into your middleware chain:
import { createStore, applyMiddleware } from 'redux'
import createApiMiddleman from 'redux-api-middleman'
let apiMiddleware = createApiMiddleman({
baseUrl: 'http://api.myapp.com',
})
const store = applyMiddleware(
[ apiMiddleware ]
)(createStore)()
- Use it in your action creators:
// user action
import { CALL_API } from 'redux-api-middleman'
export const GETTING_MY_INFO = 'GETTING_MY_INFO'
export const GET_MY_INFO_SUCCESS = 'GET_MY_INFO_SUCCESS'
export const GET_MY_INFO_FAILED = 'GET_MY_INFO_FAILED'
export function getMyInfo() {
return {
[CALL_API]: {
method: 'get',
path: '/me',
sendingType: GETTING_MY_INFO,
successType: GET_CONTRACTS_SUCCESS,
errorType: GET_MY_INFO_FAILED
}
}
}
- Handle it in your reducer:
// user reducer
import { GET_CONTRACTS_SUCCESS } from 'actions/users'
const defaultState = {}
export default function(state = defaultState, action) {
switch(action.type) {
case GET_CONTRACTS_SUCCESS:
return action.response
default:
return state
}
}
The code above would send a GET
request to http://api.myapp.com/me
,
when success, it would dispatch an action:
{
type: GET_CONTRACTS_SUCCESS,
response: { the-camelized-response-body }
}
- Async to Sync: Abstract the async nature of sending API to make it easier to implement/test
- Universal Rendering Friendly
- Support chaining(successive) API calls
- Side Effect Friendly
- Replay request optionally when failed
- Tweek request/response format when needed
A middleware can be created like this:
import apiMiddleware from 'redux-api-middleman'
apiMiddleware({
baseUrl: 'https://api.myapp.com',
errorInterceptor: ({ err, proceedError, replay, getState })=> {
// handle replay here
},
generateDefaultParams: ({ getState })=> {
return {
headers: { 'X-Requested-From': 'my-killer-app' },
}
},
maxReplayTimes: 5
})
When provided, this function would be invoked whenever an API call fails. The function signature looks like this:
({ err, proceedError, replay, getState })=> {
}
Where:
err
is the error object returned by superagent
,
replay()
can be used to replay the request with the same method/parameters,
proceedError()
can be used to proceed error to reducers
For example, to refresh access token when server responds 401:
({ err, proceedError, replay, getState })=> {
if(err.status === 401) {
refreshAccessToken().then((res)=> {
// here you can pass additional headers if you want
let headers = {
'x-access-token': res.token,
}
replay({ headers })
})
} else {
proceedError()
}
}
The code above would do the token refreshing whenever err is 401, and proceed the original error otherwise.
A function which takes ({ getState })
and returns an object like this:
{
headers: { 'x-header-key': 'header-val' },
query: { queryKey: 'query-val' },
body: { bodyKey: 'body-val' }
}
On each request, the object returned by this function would be merged into the request's header
, query
, and body
, respectively.
In Action Creators, we can use the following code to send a single request:
import { CALL_API } from 'redux-api-middleman'
export const ON_REQUEST_SUCCESS = 'ON_REQUEST_SUCCESS'
export const ON_REQUEST_FAILED = 'ON_REQUEST_FAILED'
export const ON_SENDING_REQUEST = 'ON_SENDING_REQUEST'
export function getInfo({ username }) {
return {
extraKey: 'extra-val',
[CALL_API]: {
method: 'get',
path: `/users/${username}/info`,
successType: ON_REQUEST_SUCCESS,
errorType: ON_REQUEST_FAILED,
sendingType: ON_REQUEST_FAILED,
afterSuccess: ({ getState, dispatch, response }) => {
//...
},
afterError: ({ getState, error })=> {
//...
}
}
}
}
In short, just return an action object with CALL_API
.
Http verb to use, can be get
, post
, put
or del
Request path to be concated with baseUrl
Full url of request, will take precedence over path
and will ignore baseUrl
Camelize response keys of the request. default to true
Transform { user_name: 'name' }
to { userName: 'name' }
Decamelize request payload keys. default to true
Transform { userName: 'name' }
to { user_name: 'name' }
Enable Access-Control requests or not. default to true
Action type to be dispatched immediately after sending the request
Action type to be dispatched after the API call success
Action type to be dispatched after the API call fails
A callback function to be invoked after dispatching the action with type successType
.
({ getState, dispatch, response })
would be passed into this callback function.
This is a good place to handle request-related side effects such as route pushing.
A callback function to be invoked after dispatching the action with type errorType
.
({ getState, error })
would be passed into this callback function.
To send chaining requests, just return an action with CHAIN_API
-keyed object like this:
import { CALL_API, CHAIN_API } from 'redux-api-middleman'
export const ON_REQUEST_SUCCESS1 = 'ON_REQUEST_SUCCESS1'
export const ON_REQUEST_SUCCESS2 = 'ON_REQUEST_SUCCESS2'
export function getInfo({ username }) {
return {
[CHAIN_API]: [
()=> {
return {
extraKey: 'extra-val',
[CALL_API]: {
method: 'get',
path: `/users/${username}/info`,
successType: ON_REQUEST_SUCCESS1
}
}
},
(responseOfFirstReq)=> {
return {
[CALL_API]: {
method: 'get',
path: `/blogs/${responseOfFirstReq.blogId}`,
successType: ON_REQUEST_SUCCESS2
}
}
}
]
}
}
In the code above, we send an API to /users/${username}/info
to fetch user info containing a key blogId
.
After the first request is finished, we then send the second request with the blogId
returned by server.
During the life cycle of an API call, several types of actions would be dispatched:
After the request has been sent, an action of type sendingType
would be dispatched immediately.
The action would contain the key-val pairs other than CALL_API
in the action object.
For example, if our action object looks like this:
{
extraKey1: 'extra-val-1',
extraKey2: 'extra-val-2',
[CALL_API]: {
...
}
}
then the sendingType
action would be:
{
type: sendingType,
extraKey1: 'extra-val-1',
extraKey2: 'extra-val-2'
}
After the server responds successfully, an action of type successType
would be dispatched.
The action would contain:
- the key-val pairs other than
CALL_API
in the action object - an extra
response
key, with its value be the server response
For example, if the server responds with a body like this:
{
responseKey: 'response-val'
}
then the successType
action would be:
{
type: successType,
extraKey1: 'extra-val-1',
extraKey2: 'extra-val-2',
response: {
responseKey: 'response-val'
}
}
After the server responds fails, an action of type errorType
would be dispatched.
The action would contain:
- the key-val pairs other than
CALL_API
in the action object - an extra
error
key, with its value be the error object returned byaxios
MIT