redux-shapeshifter-middleware
v1.3.5
Published
Redux middleware for empowering your actions using `axios` and `qs` libraries combined.
Downloads
256
Maintainers
Readme
redux-shapeshifter-middleware
Redux middleware that will empower your actions to become your go-to guy whenever there is a need for ajax calls ... and have you say, ...!
Table of Contents
Installation
$ npm install redux-shapeshifter-middleware
# or
$ yarn add redux-shapeshifter-middleware
Implementation
A very basic implementation.
import { createStore, applyMiddleware } from 'redux';
import shapeshifter from 'redux-shapeshifter-middleware';
const apiMiddleware = shapeshifter({
base: 'http://api.url/v1/',
/**
* If ACTION.payload.auth is set to `true` this will kick in and add the
* properties added here to the API request.
*
* Note: These values will be taken from Redux store
* e.g. below would result in:
* Store {
* user: {
* sessionid: '1234abcd'
* }
* }
*/
auth: {
user: 'sessionid',
},
fallbackToAxiosStatusResponse: true, // default is: true
// Above tells the middleware to fallback to Axios status response if
// the data response from the API call is missing the property `status`.
//
// If you however would like to deal with the status responses yourself you might
// want to set this to false and then in the response object from your back-end
// always provide a `status` property.
useOnlyAxiosStatusResponse: true, // default is: false
// Above would ignore `fallbackToAxiosStatusResponse` and
// `customSuccessResponses` if set to true. This means that we will use
// Axios response object and its status code instead of relying on one
// passed to the response.data object, or fallbacking to response.status
// if response.data.status is missing.
useETags: false, // default is: false
})
const store = createStore(
reducers,
applyMiddleware(
// ... other middlewares
someMiddleware,
apiMiddleware,
),
)
A more detailed set up of shapeshifter and authentication.
import { createStore, applyMiddleware } from 'redux';
import shapeshifter from 'redux-shapeshifter-middleware';
const shapeshifterOpts = {
base: 'http://api.url/v1/',
auth: {
user: {
sessionid: true,
// If you wish to make sure a property is NOT present
// you may pass `false` instead.. note that this means
// the back-end would have to deal with the incoming data.
},
},
/**
* constants.API
* Tells the middleware what action type it should act on
*
* constants.API_ERROR
* If back-end responds with an error or call didn't go through,
* middleware will emit 'API_ERROR'.. Unless you specified your own
* custom 'failure'-method within the 'payload'-key in your action.
* ```
* return {
* type: API_ERROR,
* message: "API/FETCH_ALL_USERS failed.. lol",
* error: error // error from back-end
* }
* ```
*
* constants.API_VOID
* Mainly used within generator functions, if we don't end
* the generator function with a `return { type: SOME_ACTION }`.
* Then the middleware will emit the following:
* ```
* return {
* type: API_VOID,
* LAST_ACTION: 'API/FETCH_ALL_USERS' // e.g...
* }
* ```
*/
constants: {
API : 'API_CONSTANT', // default: 'API'
API_ERROR : 'API_ERROR_RESPONSE', // default: 'API_ERROR'
API_VOID : 'API_NO_RESPONSE', // default: 'API_VOID'
}
}
const apiMiddleware = shapeshifter( shapeshifterOpts )
const store = createStore(
reducers,
applyMiddleware(
// ... other middlewares
someMiddleware,
apiMiddleware,
),
)
Header authentication
import { createStore, applyMiddleware } from 'redux';
import shapeshifter from 'redux-shapeshifter-middleware';
const shapeshifterOpts = {
base: 'http://api.url/v1/',
auth: {
headers: {
'Authorization': 'Bearer #user.token',
// Above will append the key ("Authorization") to each http request being made
// that has the `ACTION.payload.auth` set to true.
// The value of the key has something weird in it, "#user.token". What this means is
// that when the request is made this weird part will be replaced with the actual
// value from the Redux store.
//
// e.g. this could be used more than once, or it could also be just for deeper values
// 'Bearer #user.data.private.token'.
},
},
// .. retracted code, because it's the same as above.
}
// .. retracted code, because it's the same as above.
Middleware configuration
All options that the middleware can take.
base <string>
default: ''
This sets the base url for all API calls being made through this middleware. Could be overwritten by using the ACTION.axios.baseURL
property on the Action.
constants <object>
API
<string>
default: 'API'
This is the type this middleware will look for when actions are being dispatched.
API_ERROR
<string>
default: 'API_ERROR'
When an http request fails, this is the type that will be dispatched and could be used to return a visual response to the end-user e.g. on a failed login attempt.
API_VOID
<string>
default: undefined
Upon success of a generator function we have the choice to pass a type of our own, if the return statement is omitted or if there is no returned object with a key
type
then this will be dispatched as thetype
inside an object, along with another keyLAST_ACTION
which references the type that initiated the process.
auth <object>
default: undefined
When making a request you can pass the ACTION.payload.auth <boolean>
property to ACTION.payload <object>
, doing this will activate this object which in return will pass the value as a parameter to the request being made.
Note that any properties or values passed within the auth {} object are connected to the Store.
It is not possible to mix Example 1 and 2 with Example 3
Example 1 with a shallow value to check:
const apiMiddleware = shapeshifter({
// .. retracted code
auth: {
user: 'sessionid',
},
})
Looking at Example 1 it would on any HTTP request being made with ACTION.payload.auth = true
would check the Store for the properties user
and within that sessionid
and pass the value found to the request as a parameter.
Example 2 with a nested value to disallow:
const apiMiddleware = shapeshifter({
// .. retracted code
auth: {
user: 'sessionid',
profile: {
account: {
freeMember: false,
},
},
},
})
Passing a boolean
as the value will check that the property does not exist on the current Store, if it does a warning will be emitted and the request will not be made. Could be done the other way around, if you pass true
it would be required to have that property in the Store.. although it would be up to the back-end to evaluate the value coming from the Store in that case.
Example 3 with a nested property and headers authorization:
const apiMiddleware = shapeshifter({
// .. retracted code
auth: {
headers: {
'Authorization': 'Bearer #user.token',
// or even deeper
'Authorization': 'Bearer #user.data.private.token',
// or even multiple values
'custom-header': 'id=#user.id name=#user.private.name email=#user.private.data.email',
},
},
})
Example 3 allows us to pass headers for authorization on requests having the ACTION.payload.auth
set to true.
useETags <boolean>
default: false
This will enable the middleware to store ETag(s) if they exist in the response with the URI segments as the key.
dispatchETagCreationType <string>
default: undefined
Requires useETags
to be set to true.
When the middleware handles a call it will check if the response has an ETag header set, if it does, we store it. Though as we store it we will also emit the given value set to dispatchETagCreationType
so that it's possible to react when the middleware stores the call and its ETag value.
Example of action dispatched upon storing of ETag:
{
type: valuePassedTo_dispatchETagCreationType,
ETag: 'randomETagValue',
key: '/fetch/users/',
}
matchingETagHeaders <function>
default: undefined
- Arguments
obj <object>
ETag <string>
dispatch <function>
state <object>
getState <function>
Requires useETags
to be set to true.
Takes a function which is called when any endpoint has an ETag stored (which is done by the middleware if the response holds an ETag property). The function receives normal store operations as well as the matching ETag
identifier for you to append to the headers you wish to pass.
If nothing passed to this property the following will be the default headers passed if the call already has stored an ETag:
{
'If-None-Match': 'some-etag-value',
'Cache-Control': 'private, must-revalidate',
}
handleStatusResponses <function>
default: null
- Arguments
- response
<object>
The Axios response object. - store
<object>
#dispatch() <function>
#getState <function>
#state <object>
- response
NOTE that this method must return either Promise.resolve()
or Promise.reject()
depending on your own conditions..
Defining this method means that any customSuccessResponses
defined or any error handling done by the middleware will be ignored.. It's now up to you to deal with that however you like. So by returning a Promise.reject()
the *_FAILURE
Action would be dispatched or vice versa if you would return Promise.resolve()
..
Example
const apiMiddleware = shapeshifter({
// .. retracted code
handleStatusResponses(response, store) {
if ( response.data && response.data.errors ) {
// Pass the error message or something similar along with the failed Action.
return Promise.reject( response.data.errors )
}
// No need to pass anything here since the execution will continue as per usual.
return Promise.resolve()
}
})
fallbackToAxiosStatusResponse <boolean>
default: true
If you've built your own REST API and want to determine yourself what's right or wrong then setting this value to false would help you with that. Otherwise this would check the response object for a status
key and if none exists it falls back to what Axios could tell from the request made.
customSuccessResponses <array>
default: null
In case you are more "wordy" in your responses and your response object might look like:
{
user: {
name: 'DAwaa'
},
status: 'success'
}
Then you might want to consider adding 'success' to the array when initializing the middleware to let it know about your custom success response.
useOnlyAxiosStatusResponse <boolean>
default: false
This ignores fallbackToAxiosStatusResponse
and customSuccessResponses
, this means it only looks at the status code from the Axios response object.
emitRequestType <boolean>
default: false
By default redux-shapeshifter-middleware
doesn't emit the neutral action type. It returns either the *_SUCCESS
or *_FAILED
depending on what the result of the API call was.
By setting emitRequestType
to true
the middleware will also emit YOUR_ACTION
along with its respective types, YOUR_ACTION_SUCCESS
and YOUR_ACTION_FAILED
based on the situation.
useFullResponseObject <boolean>
default: false
By default redux-shapeshifter-middleware
actions will upon success return response.data
for you to act upon, however sometimes it's wanted to actually have the entire response
object at hand. This option allows to define in one place if all shapeshifter actions should return the response
object.
However if you're only interested in some actions returning the full response
object you could have a look at ACTION.payload.useFullResponseObject
to define it per action instead.
warnOnCancellation <boolean>
default: false
By default when cancelling axios
calls the dependency itself will throw an error with a user-defined reason to why it was canceled. This behavior could be unwanted if let's say you're using an error-catching framework that records and logs all client errors that occurs in production for users. It's not likely that everyone would consider a canceled call "serious" enough to be an error. In this case configuring this option to true
then only a console.warn(reason)
will be emitted to the console.
throwOnError <boolean>
default: false
By default shapeshifter
will not bubble up errors but instead swallow them and dispatch *_FAILURE
(or what you decide to call your failure actions) actions along with logging the error to the console. Setting this option to true
will no longer log errors but instead throw them, which requires a .catch()
method to be implemented to avoid unhandled promise rejection
errors.
This can also be done on ACTION level in the case you don't want all actions to throw but only one or few ones.
axios <object>
default: undefined
Note
Any property defined under axios
will be overridden by ACTION.axios
if the same property appears in both.
In the case you want to have a global configuration for all of your ACTIONs handled by shapeshifter this is the right place to look at. What you'll be able to access through this can be seen under Axios documentation.
Action properties
We will explore what properties there are to be used for our new actions..
A valid shapeshifter action returns a Promise
.
ACTION.type <string>
Nothing unusual here, just what type we send out to the system.. For the middleware to pick it up, a classy 'API' would do, unless you specified otherwise in the set up of shapeshifter.
const anActionFn = () => ({
type: 'API', // or API (without quotation marks) if you're using a constant
...
})
ACTION.types <array>
An array containing your actions
const anActionFn = () => ({
type: 'API',
types: [
WHATEVER_ACTION,
WHATEVER_ACTION_SUCCESS,
WHATEVER_ACTION_FAILED,
],
...
})
ACTION.method <string>
default: 'get'
const anActionFn = () => ({
type: 'API',
types: [
WHATEVER_ACTION,
WHATEVER_ACTION_SUCCESS,
WHATEVER_ACTION_FAILED,
],
method: 'post', // default is: get
...
})
ACTION.payload <function>
- Arguments
- store
<object>
#dispatch() <function>
#state <object>
- store
This property and its value is what actually defines the API call we want to make.
Note Payload must return an object. Easiest done using a fat-arrow function like below.
const anActionFn = () => ({
type: 'API',
types: [
WHATEVER_ACTION,
WHATEVER_ACTION_SUCCESS,
WHATEVER_ACTION_FAILED,
],
payload: store => ({
}),
// or if you fancy destructuring
// payload: ({ dispatch, state }) => ({})
Inside payload properties
Acceptable properties to be used by the returned object from ACTION.payload
const anActionFn = () => ({
type: 'API',
types: [
WHATEVER_ACTION,
WHATEVER_ACTION_SUCCESS,
WHATEVER_ACTION_FAILED,
],
payload: store => ({
// THE BELOW PROPERTIES GO IN HERE <<<<<<
}),
ACTION.payload.url <string>
ACTION.payload.params <object>
ACTION.payload.tapBeforeCall <function>
- Arguments
obj <object>
params <object>
dispatch <function>
state <object>
getState <function>
Is called before the API request is made, also the function receives an object argument.
ACTION.payload.success <function>
- Arguments
type <string>
payload <object>
- Do note that by default the middleware returns the result from
response.data
. If you want the fullresponse
object, have a look atmiddleware.useFullResponseObject
or per actionACTION.payload.useFullResponseObject
- Do note that by default the middleware returns the result from
meta|store <object>
- If
meta
key is missing from the first level of the API action, then this 3rd argument will be replaced withstore
.
- If
store <object>
-- Will be 'null' if nometa
key was defined in the first level of the API action.
This method is run if the API call went through successfully with no errors.
ACTION.payload.failure <function>
- Arguments
type <string>
error <mixed>
This method is run if the API call responds with an error from the back-end.
ACTION.payload.repeat <function>
- Arguments
response <object>
The Axios response objectresolve <function>
reject <function>
Inside the repeat
-function you will have the Axios response object at hand to determine yourself when you want to pass either the *_SUCCESS
or *_FAILED
action.
There are two primary ways to denote an action from this state, either returning a boolean
or calling one of the two other function arguments passed to repeat()
, namely resolve
and reject
.
Returning a boolean from ACTION.payload.repeat
will send the Axios response object to either the ACTION.payload.success
or ACTION.payload.failure
method of your API action as the payload.
However if you denote your action using either resolve
or reject
, whatever passed to either of these two will be the payload sent to ACTION.payload.success
or ACTION.payload.failure
.
Example using boolean
// Returning a boolean
const success = () => { /* retracted code */}
const failure = () => { /* retracted code */}
export const fetchUser = () => ({
type: API,
types: [
FETCH_USER,
FETCH_USER_SUCCESS,
FETCH_USER_FAILED,
],
payload: () => ({
url: '/users/user/fetch',
success,
failure,
interval: 100,
repeat: (response) => {
const { data } = response
if (data && data.user && data.user.isOnline) {
return true // This tells the middleware to call
// the `success`-method defined above
// with the Axios response object.
//
// Same thing would've happened if one
// were to return `false`, however the
// `failure`-method would be called instead.
}
}
})
})
Example using custom payload
// Returning custom payload
const success = () => { /* retracted code */}
const failure = () => { /* retracted code */}
export const fetchUser = () => ({
type: API,
types: [
FETCH_USER,
FETCH_USER_SUCCESS,
FETCH_USER_FAILED,
],
payload: () => ({
url: '/users/user/fetch',
success,
failure,
interval: 100,
repeat: (response, resolve, reject) => {
const { data } = response
if (data && data.user && data.user.isOnline) {
return resolve({ userIsOnline: true }) // Here we return and call
// `resolve`-method with a
// custom payload. This will
// like above example call the
// `success`-method with the given
// value passed to `resolve` as the
// payload for `success`.
//
// Vice versa if one were to call
// `reject`-method instead with a
// custom payload, the `failure`-
// method would be called and the
// passed value would be the payload.
}
}
})
})
ACTION.payload.interval <integer>
default: 5000
This is used in combination with the ACTION.payload.repeat
function. How often we should be calling the given endpoint.
ACTION.payload.tapAfterCall <function>
- Arguments
obj <object>
params <object>
dispatch <function>
state <object>
getState <function>
Same as ACTION.payload.tapBeforeCall <function>
but is called after the API request was made however not finished.
ACTION.payload.auth <boolean>
default: false
If the API call is constructed with auth: true
and the middleware set up was initialized with an auth
key pointing to the part of the store you want to use for authorization in your API calls. Then what you set up in the initialization will be added to the requests parameters automatically for you.
ACTION.payload.ETagCallback <object|function>
default: undefined
Requires useETags
to be set to true.
When a call is made and the response has already been cached as the resource hasn't changed since last time. We will emit either an object if passed to ETagCallback
or run a function if provided.
If a function is provided the fuction will receive following arguments:
- Arguments
obj <object>
type <string>
The neutral type is return, e.g.
FETCH_USER
and not any of the ones that has suffix_SUCCESS
or_FAILED
.path <string>
The path called, e.g.
/fetch/users
.ETag <string>
The ETag used resulting in a 304 response.
dispatch <function>
state <object>
getState <function>
ACTION.payload.useFullResponseObject <boolean>
default: false
In the case you still want the middleware to return response.data
for your other actions but only one or few should return the full response
object you could set this property to true
and the action will in it's success
-method return the full response
object.
ACTION.payload.throwOnError <boolean>
default: false
By default shapeshifter
will not bubble up errors but instead swallow them and dispatch *_FAILURE
(or what you decide to call your failure actions) actions along with logging the error to the console. Setting this option to true
will no longer log errors but instead throw them, which requires a .catch()
method to be implemented to avoid unhandled promise rejection
errors.
ACTION.meta <object>
This is our jack-in-the-box prop, you can probably think of lots of cool stuff to do with this, but below I will showcase what I've used it for.
Basically this allows to bridge stuff between the action and the ACTION.payload.success()
method.
Note
Check ACTION.payload.success
above to understand where these meta tags will be available.
const success = (type, payload, meta, store) => ({
// We can from here reach anything put inside `meta` property
// inside the action definition.
type: type,
heeliesAreCool: meta.randomKeyHere.heeliesAreCool,
})
const fetchHeelies = () => ({
type: 'API',
types: [
FETCH_HEELIES,
FETCH_HEELIES_SUCCESS,
FETCH_HEELIES_FAILED,
],
payload: store => ({
url: '/fetch/heelies/',
params: {
color: 'pink',
},
success: success,
}),
meta: {
randomKeyHere: {
heeliesAreCool: true,
},
},
ACTION.meta.mergeParams <boolean>
default: false
Just like this property states, it will pass anything you have under the property ACTION.payload.params
to the ACTION.meta
parameter passed to ACTION.payload.success()
method.
ACTION.axios <object>
This parameter allows us to use any Axios Request Config property that you can find under their docs.. here.
Anything added under the ACTION.axios<object>
will have higher priority, meaning
that it will override anything set before in the payload object that has the
same property name.
How to use?
Normal example
A normal case where we have both dispatch and our current state for our usage.
// internal
import { API } from '__actions__/consts'
export const FETCH_ALL_USERS = 'API/FETCH_ALL_USERS'
export const FETCH_ALL_USERS_SUCCESS = 'API/FETCH_ALL_USERS_SUCCESS'
export const FETCH_ALL_USERS_FAILED = 'API/FETCH_ALL_USERS_FAILED'
// @param {string} type This is our _SUCCESS constant
// @param {object} payload The response from our back-end
const success = (type, payload) => ({
type : type,
users : payload.items
})
// @param {string} type This is our _FAILED constant
// @param {object} error The error response from our back-end
const failure = (type, error) => ({
type : type,
message : 'Failed to fetch all users.',
error : error
})
export const fetchAllUsers = () => ({
type: API,
types: [
FETCH_ALL_USERS,
FETCH_ALL_USERS_SUCCESS,
FETCH_ALL_USERS_FAILED
],
method: 'get', // default is 'get' - this could be omitted in this case
payload: ({ dispatch, state }) => ({
url: '/users/all',
success: success,
failure: failure
})
})
Generator example
A case where we make us of a generator function.
// internal
import { API } from '__actions__/consts'
export const FETCH_USER = 'API/FETCH_USER'
export const FETCH_USER_SUCCESS = 'API/FETCH_USER_SUCCESS'
export const FETCH_USER_FAILED = 'API/FETCH_USER_FAILED'
// @param {string} type This is our _SUCCESS constant
// @param {object} payload The response from our back-end
// @param {object} store - { dispatch, state, getState }
const success = function* (type, payload, { dispatch, state }) {
// Get the USER id
const userId = payload.user.id
// Fetch name of user
const myName = yield new Promise((resolve, reject) => {
axios.get('some-weird-url', { id: userId })
.then((response) => {
// Pretend all is fine and we get our name...
resolve( response.name );
})
})
dispatch({ type: 'MY_NAME_IS_WHAT', name: myName })
// Conditionally if we want to emit to the
// system that the call is done.
return {
type,
}
// Otherwise the middleware itself would emit
return {
type: 'API_VOID',
LAST_ACTION: 'FETCH_USER',
}
}
// @param {string} type This is our _FAILED constant
// @param {object} error The error response from our back-end
const failure = (type, error) => ({
type : type,
message : 'Failed to fetch all users.',
error : error,
})
export const fetchAllUsers = userId => ({
type: API,
types: [
FETCH_USER
FETCH_USER_SUCCESS,
FETCH_USER_FAILED
],
method: 'get', // default is 'get' - this could be omitted in this case
payload: ({ dispatch, state }) => ({
url: '/fetch-user-without-their-name',
params: {
id: userId
},
success: success,
failure: failure
}),
})
Chain example
Just like the normal example
but this illustrates it can be chained.
// ... same code as the normal example
export const fetchAllUsers = userId => ({
... // same code as the normal example
})
// another-file.js
import { fetchAllUsers } from './somewhere.js';
fetchAllUsers()
.then(response => {
// this .then() happens after the dispatch of `*_SUCCESS` has happened.
// here you have access to the full axios `response` object
})
.catch(error => {
// this .catch() happens after the dispatch of `*_FAILED` has happened.
// here you have access to the `error` that was thrown
})