Sign Up for Free

RunKit +

Try any Node.js package right in your browser

This is a playground to test code. It runs a full Node.js environment and already has all of npm’s 1,000,000+ packages pre-installed, including redux-action-handlers with all npm packages installed. Try it out:

var reduxActionHandlers = require("redux-action-handlers")

This service is provided by RunKit and is not affiliated with npm, Inc or the package authors.

redux-action-handlers v1.0.1

Redux actionHandlers object

ReduxActionHandlers

ActionHandlers library to improve the Redux development experience

Node version

https://nodei.co/npm/redux-action-handlers.png?downloads=true&downloadRank=true&stars=true

contributions welcome

How to use this:

  • Put all Your reducer cases into functions, and put them in the action file

src/actions/downloadSomething.js

export function downloadSomethingRequest_actionHandler(state, action) {
  return {
    ...state,
    registration: action.payload,
    isFetching: true,
    data: {}
  }
}
  • Add them to actionHandlers (for reducer "something", map "DOWNLOAD_SOMETHING_REQUEST" action type to "downloadSomethingRequest_actionHandler" function)
actionHandlers.add('something', DOWNLOAD_SOMETHING_REQUEST, downloadSomethingRequest_actionHandler)
  • Add handling to reducer
const reducer = (state = initialState, action = {}) => {
  return actionHandlers.runReducer('something', state, action);
}

Benefits of using this library:

  • One file describing all the functionality of an action. Instead of splitting the action creator logic and reducer logic to two separate files, one concern is in one file.
  • Huge reducers are no longer a problem. Instead of having all the reducer handling code in one huge switch...case block, each action is in it's own function which makes it cleaner.
  • When using combineReducers, You create shards. Those shards are enforced in actionHandlers. This means that You will never have a polluted root reducer namespace

Compatibility:

  • This library has no dependencies
  • It does not force the ActionHandlers on Your whole project. You can still use both classic reducer switch...case and ActionHandlers.
  • It does not pollute the redux state with functions.
  • It's a clean library - it supports all use cases.
  • Supports nested combineReducers
  • Supports many-to-many relationships between actions and reducer cases.
  • It's tiny. All it takes is 31 lines of code.
  • Testing is unchanged, but can be optimized. By testing the reducer functions directly, tests can become much clearer, but this library doesn't force it. You can test the whole reducer.

ReduxActionHandlers reason to exist

To create a single action and a reducer, you have to create multiple files and put a part of the implementation in each of them:

src/actions/downloadSomething_types.js

export const DOWNLOAD_SOMETHING_REQUEST = '@request/DOWNLOAD_SOMETHING_REQUEST'
export const DOWNLOAD_SOMETHING_SUCCESS = '@request/DOWNLOAD_SOMETHING_SUCCESS'
export const DOWNLOAD_SOMETHING_FAILURE = '@request/DOWNLOAD_SOMETHING_FAILURE'

src/actions/downloadSomething.js

import {
  DOWNLOAD_SOMETHING_REQUEST,
  DOWNLOAD_SOMETHING_SUCCESS,
  DOWNLOAD_SOMETHING_FAILURE
} from 'src/actions/downloadSomething_types.js'

export default function downloadSomethingAction() {
  return (dispatch) => {
    dispatch({
      type: DOWNLOAD_SOMETHING_REQUEST
    })

    return api.downloadSomething()
      .then(
        json => dispatch({
          type: DOWNLOAD_SOMETHING_SUCCESS,
          payload: json
        }),
        exception => {
          dispatch({
            type: DOWNLOAD_SOMETHING_FAILURE
          })
          dispatch(notifyError(exception.message))
        }
      )
  }
}

src/reducers/downloadReducer.js

import {
  DOWNLOAD_SOMETHING_REQUEST,
  DOWNLOAD_SOMETHING_SUCCESS,
  DOWNLOAD_SOMETHING_FAILURE
} from 'actions/downloadSomething_types.js'

export const initialState = {
  isFetching: false,
  data: {},
  registration: null,
  visible: false
};

const reducer = (state = initialState, action = {}) => {

  if (!action.type) return state

  switch (action.type) {

    case DOWNLOAD_SOMETHING_REQUEST:
      return {
        ...state,
        registration: action.payload,
        isFetching: true,
        data: {}
      }

    case DOWNLOAD_SOMETHING_SUCCESS:
      return {
        ...state,
        data: action.payload,
        isFetching: false
      }

    case DOWNLOAD_SOMETHING_FAILURE:
      return {
        ...state,
        data: {},
        isFetching: false
      }

    default:
      return state

  }
}

export default reducer

This single concern, single chunk of functionality is spread over three separate files.

It makes it difficult to reason about and debug. The reducer construction is also an issue because it very easily can grow into a monster like this (and this has a potential to be much bigger):

import {
  OPEN_INFO_PANEL,
  CLOSE_INFO_PANEL,
  READ_INFO_REQUEST,
  READ_INFO_SUCCESS,
  READ_INFO_FAILURE,
  UPDATE_TIMES_REQUEST,
  UPDATE_TIMES_SUCCESS,
  UPDATE_TIMES_FAILURE,
  UPDATE_POSITION_STAND_REQUEST,
  UPDATE_POSITION_STAND_SUCCESS,
  UPDATE_POSITION_STAND_FAILURE
} from 'features/info/actions'


export const defaultState = {
  data: {},
  flightId: null,
  isFetching: false,
  isUpdating: false,
  visible: false
}

export default (state = defaultState, action = {}) => {

  if (!action.type) return state

  switch(action.type) {

    case OPEN_INFO_PANEL:
      return {
        ...state,
        flightId: action.payload,
        isFetching: false,
        visible: true
      }

    case CLOSE_INFO_PANEL:
      return {
        ...state,
        flightId: null,
        isFetching: false,
        visible: false
      }

    case READ_INFO_REQUEST:
      return {
        ...state,
        data: {},
        flightId: action.payload,
        isFetching: true
      }

    case READ_INFO_SUCCESS:
      return {
        ...state,
        data: action.payload,
        isFetching: false
      }

    case READ_INFO_FAILURE:
      return {
        ...state,
        data: {},
        isFetching: false
      }

    case UPDATE_TIMES_SUCCESS:
      return {
        ...state,
        isUpdating: false,
        data: { ...state.data,
          times: {...state.data.times,
            actual: {...action.payload.actualTimes}
          }
        }
      }

    case UPDATE_POSITION_STAND_SUCCESS:
      return {
        ...state,
        isUpdating: false,
        data: {
          ...state.data,
          gatesInfo: {
            ...action.payload
          }
        }
      }

    case UPDATE_TIMES_REQUEST:
      return {
        ...state,
        isUpdating: true
      }

    case UPDATE_POSITION_STAND_REQUEST:
      return {
        ...state,
        isUpdating: true
      }

    case UPDATE_TIMES_FAILURE:
      return {
        ...state,
        isUpdating: false
      }

    case UPDATE_POSITION_STAND_FAILURE:
      return {
        ...state,
        isUpdating: false
      }

    default:
      return state
  }
}

Solution:

ReduxActionHandlers!

Idea:

Put all the reducer cases into functions like so:

Before:

case DOWNLOAD_SOMETHING_REQUEST:
  return {
    ...state,
    registration: action.payload,
    isFetching: true,
    data: {}
  }

After:

function downloadSomethingRequest_actionHandler(state, action) {
  return {
    ...state,
    registration: action.payload,
    isFetching: true,
    data: {}
  }
}

Then, instead of attaching actions externally ( from reducer ) we would attach actions to a reducer like so:

actionHandlers.add('something', DOWNLOAD_SOMETHING_REQUEST, downloadSomethingRequest_actionHandler)

Resulting files would be as follows:

src/actions/downloadSomething.js

import actionHandlers from 'redux-action-handlers'

const DOWNLOAD_SOMETHING_REQUEST = '@request/DOWNLOAD_SOMETHING_REQUEST'
const DOWNLOAD_SOMETHING_SUCCESS = '@request/DOWNLOAD_SOMETHING_SUCCESS'
const DOWNLOAD_SOMETHING_FAILURE = '@request/DOWNLOAD_SOMETHING_FAILURE'

export function downloadSomethingRequest_actionHandler(state, action) {
  return {
    ...state,
    registration: action.payload,
    isFetching: true,
    data: {}
  }
}

export function downloadSomethingSuccess_actionHandler(state, action) {
  return {
    ...state,
    data: action.payload,
    isFetching: false
  }
}

export function downloadSomethingFailure_actionHandler(state, action) {
  return {
    ...state,
    data: {},
    isFetching: false
  }
}

export default function downloadSomethingAction() {
  return (dispatch) => {
    dispatch({
      type: DOWNLOAD_SOMETHING_REQUEST
    })

    return api.downloadSomething()
      .then(
        json => dispatch({
          type: DOWNLOAD_SOMETHING_SUCCESS,
          payload: json
        }),
        exception => {
          dispatch({
            type: DOWNLOAD_SOMETHING_FAILURE
          })
          dispatch(notifyError(exception.message))
        }
      )
  }
}

actionHandlers.add('something', DOWNLOAD_SOMETHING_REQUEST, downloadSomethingRequest_actionHandler)
actionHandlers.add('something', DOWNLOAD_SOMETHING_SUCCESS, downloadSomethingSuccess_actionHandler)
actionHandlers.add('something', DOWNLOAD_SOMETHING_FAILURE, downloadSomethingFailure_actionHandler)

src/reducers/downloadSomethingReducer.js

import actionHandlers from 'redux-action-handlers'

export const initialState = {
  isFetching: false,
  data: {},
  registration: null,
  visible: false
};

const reducer = (state = initialState, action = {}) => {
  return actionHandlers.runReducer('something', state, action);
}

export default reducer
RunKit is a free, in-browser JavaScript dev environment for prototyping Node.js code, with every npm package installed. Sign up to share your code.
Sign Up for Free