redux-shapeshifter-middleware

1.3.5 • Public • Published

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 the type inside an object, along with another key LAST_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>

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>

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>
    • meta|store <object>
      • If meta key is missing from the first level of the API action, then this 3rd argument will be replaced with store.
    • store <object> -- Will be 'null' if no meta 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

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
    })

Package Sidebar

Install

npm i redux-shapeshifter-middleware

Weekly Downloads

52

Version

1.3.5

License

MIT

Unpacked Size

91.6 kB

Total Files

35

Last publish

Collaborators

  • dawaa