@touchtribe/redux-helpers

2.0.1 • Public • Published

@touchtribe/redux-helpers

An opinionated library to create actions and reducers for Redux, heavily based on redux-act and redux-actions.

While Redux is a great library to work with, the boilerplate code needed for a basic setup is quite verbose and not prone to errors. This library aims to make defining and using actions and reducers less of a hassle.

The actions created by this library are not FSA compliant

Install

# NPM  
npm install @touchtribe/redux-helpers  
# Yarn  
yarn add @touchtribe/redux-helpers  

Content

Usage

import { createStore } from 'redux'
import { createAction, createReducer } from '@touchtribe/redux-helpers'
// default redux actionCreator signature
const increment = function () {
  return {
    type: 'INCREMENT'
  }
}
const subtract = function (amount) {
  return {
    type: 'SUBTRACT',
    amount: amount
  }
}
const decrement = createAction('dec')
const add = createAction('add', (amount) => ({ amount }))

const counterReducer = createReducer('counter', {
  'INCREMENT':
    (state) => state + 1,
  'SUBTRACT':
    (state, { amount }) => state + amount,
  [add]:
    (state, { amount }) => state + amount,
  [decrement]:
    (state) => state - 1,
})

const counterStore = createStore(counterReducer)

Api

createAction(type, identityReducer)

Parameters

  • type (string): the type of the action. Will be used as { type: type } for the resulting action.
  • identityReducer: transforms multiple arguments into an object which will be merged with the resulting action. If nothing is supplied, the first parameter of the action-creator can be an object which will be merged with the action.

Usage

Returns a new action creator. The type will be used as the action-type. If you need to support multiple arguments, you need to specify an identity reducer to merge arguments with the resulting action.

// basic action  
const incrementAction = createAction('increment')
// incrementAction()  
// -> { type: 'increment' }  
// incrementAction({ amount: 10 })  
// -> { type: 'increment', amount: 10 }  

// basic action with identityReducer  
const incrementAction = createAction('increment', (amount) => {
  return { amount: amount }
})
// incrementAction = function(amount) {  
//   return {  
//     type: 'increment',  
//     amount: amount //   }  
// }  
//  
// in short:  
const incrementAction = createAction('increment', (amount) => ({ amount }))
// incrementAction()  
// -> { type: 'increment', amount: undefined }  
// incrementAction(20)  
// -> { type: 'increment', amount: 20 }  

// incrementAction.toString() === 'increment'  
// String(incrementAction) === 'increment'  
// { [incrementAction]: 10 } === { 'increment': 10 }  

// multiple parameters  
const someAction = createAction('some', (amount, howMany) => ({ amount, howMany }))
// someAction(10, 20)  
// -> { type: 'increment', amount: 10, howMany: 20 }  

action creator

Action creators are basically functions that take arguments and return an action in the following format:

{
  type: '<action type>',
  ...identity // returned by the identity-reducer
}  

The actions returned by this library are not FSA compliant

createActionDomain(domain)

Returns a domain-prefixed createAction. Usefull if you need to have multiple actions for the resource/domain/type

Parameters

  • domain (string): The domain of the actionCreatorCreator (...giggity). Will be prefixed to the action-types.
const createUserAction = createActionDomain('user')
const fetchUser = createUserAction('fetch', (userId) => ({ userId }))
const updateUser = createUserAction('update', (userId, userData) => ({ userId, data: userData }))
// fetchUser(10)  
// -> { type: 'user//fetch', userId: 10 }  
// updateUser(10, { name: 'test user' })  
// -> { type: 'user//update', userId: 10, data: { name: 'test user' } }  

createActions(prefix, actionMap)

Returns an array mapping action types to action creators.with multiple actions of type <prefix>/<actionMapKey>.

Parameters

  • prefix (string): Will be prefixed to every action-type
  • actionMap (object): Object which keys are used as action-types and values are used as identityReducers.

Usage

let [
  fetch,
  fetchRequest,
  fetchSuccess,
  fetchFail
] = createActions('fetch', {
  init: (id) => ({ id }),
  request: (id) => ({ id }),
  success: (id, data) => ({ id, data }),
  fail: (id, error) => ({ id, error })
})
// fetch.toString() === 'fetch/init'  
// fetchRequest.toString() === 'fetch/request'  
// fetchSuccess.toString() === 'fetch/success'  
// fetchFail.toString() === 'fetch/fail'  

createActionsDomain(domain)

Returns a domain-prefixed createActions. Usefull if you need to create multiple actions, scoped on a domain.

Parameters

  • domain (string): The domain of the actionsCreatorCreator (... yes). Will be prefixed to the action-types

Usage

const createUserActions = createActionsDomain('user')
const [
  fetchUser,
  fetchUserRequest,
  fetchUserSuccess,
  fetchUserFail
] = createUserActions('fetch', {
  init: (id) => ({ id }),
  request: (id) => ({ id }),
  success: (id, data) => ({ id, data }),
  fail: (id, error) => ({ id, error })
})
// fetchUser.toString() === 'user//fetch/init'  
// fetchUserRequest.toString() === 'user//fetch/request'  
// fetchUserSuccess.toString() === 'user//fetch/success'  
// fetchUserFail.toString() === 'user//fetch/fail'  

createReducer(name, handlers, defaultState)

Parameters

  • name (string): The name of the reducer. Can later be used in your selectors or combineReducers as reducer.toString().
  • handlers (object): A map of actions and their reduce functions.
  • defaultState (any): The initial state of the reducer.

Usage

let counterReducer = createReducer(
  'counter',
  {
    'inc': (state) => state + 1
    'add': (state, action) => state + action.amount
  },
  0
)
// counterReducer.toString() === 'counter'  
// String(counterReducer) === 'counter'  
// ...  
const rootReducer = combineReducers({
  [counterReducer]: counterReducer
})
// creates a rootReducer with `counter` as a key in the store.  
// ...  
const getCounterState = (state) => state[String(counterReducer)]
// creates a selector will will return `state.counter`  

By giving the router a name, and re-using the reducer itself as the key, the application will become agnostic of the actual key in the store.

combineActions (...actionTypes)

This allows you to reduce multiple distinct actions with the same reducer.

const incrementAction = createAction('inc')
const decrementAction = createAction('dec')
const counterReducer = createReducer(
  'counter',
  {
    [combineActions('INCREMENT', incrementAction)]:
      (state) => state + 1,
    [combineActions('DECREMENT', decrementAction)]:
      (state) => state - 1,
  },
  0
)

Resolvable Async Actions

Within redux, when needing to wait for the resolution of an Action, generally redux-thunk is used, which you can use for control-flow. If you use redux-saga however, it becomes harder to act based on the outcome of a dispatched action.

This library enables you to dispatch an action and wait for the outcome, while it is being handled by a middleware.

// actions.js
export const [
  fetchUser,
  fetchUserSuccess,
  fetchUserFail
] = createResolveActions({
  init: (userId) => ({ userId }),
  resolve: (userId, user) => ({ userId, user }),
  reject: (userId, error) => ({ userId, error }),
})

// sagas.js
function * userSaga () {
  yield takeEvery(fetchUser, function * ({ resolve, reject, userId, ...action }) {
    try {
      const user = yield call(fetch, `/users/${userId}`)
      yield put(resolve(userid, user)) // put(
    } catch (error) {
      yield put(reject(userId, error))
    }
  })
}

// component.js
// store is a prop as an example implementation of redux.
function UserComponent ({ store }) {
  const [isLoading, setLoading] = useState(true)
  const [user, setUser] = useState()
  const [error, setError] = useState()
  useEffect(() => {
    setLoading(true)
    setError()
    store.dispatch(fetchUser(10))
      .then(resolveAction => setUser(resolveAction.user))
      .catch(rejectAction => setError(rejectAction.error))
      .then(() => setLoading(false))
  }, [])
  
  if (error) {
    return <div>Error: {String(error)}</div>
  }
  if (isLoading) {
    return <div>Loading...</div>
  }
  return <div>{user.name}</div>
}

// reducer.js
function reducer (state, action) {
  switch (action.type) {
    case String(fetchUserSuccess):
      return {
        ...state,
        user: action.user
      }
    case String(fetchUserFail):
      return {
        ...state,
        error: action.error
      }
    default:
      return state
  }
}

resolvableMiddleware

To enable the middleware that makes actions resolvable, it has to be added to the store using applyMiddleware.

When applied, store.dispatch(asyncInitAction) return a Promise while dispatching the action with 2 extra attributes: resolve and reject.

  • action.resolve(...args) will dispatch asyncResolveAction(...args) and then resolve the Promise with that action
  • action.reject(...args) will dispatch asyncRejectAction(...args) and then reject the Promise with that action

When not applied, store.dispatch(asyncInitAction) will just be handled as if it were a normal action.

Usage

import { createStore, applyMiddleware } from 'redux'
import { resolvableMiddleware } from '@touchtribe/redux-helpers'
import rootReducer from './reducers'

const store = createStore(
  rootReducer,
  applyMiddleware(resolvableMiddleware)
)

createResolvableActions

Works like createActions, but has slightly different signature.

  • createResolvableActions(type)
  • createResolvableActions(type, initIdentityReducer)
  • createResolvableActions(type, actionMap)

The result is equal to:

const [
  action,
  actionSucces,
  actionFail
] = createActions('type', {
  init: (payload) => ({ payload }),
  resolve: (payload) => ({ payload }),
  reject: (error) => ({ error })
})
createResolvableActions(type)

Parameters

  • type (string): Will be prefixed to every action-type
  • actionMap (object): Object which keys are used as action-types and values are used as identityReducers.

Returns

[
  initAction, // function(payload) => { type: `${type}/init`, payload, resolve: resolveAction, reject: rejectAction },
  resolveAction, // function(payload) => { type: `${type}/resolve`, payload }
  rejectAction // function(error) => { type: `${type}/resolve`, error }
]

Usage

let {  
 fetch,
 fetchSuccess, 
 fetchFail
} = createResolvableActions('fetch')  
// fetch.toString() === 'fetch/init'  
// fetchSuccess.toString() === 'fetch/resolve'  
// fetchFail.toString() === 'fetch/reject'  
createResolvableActions(type, initIdentityReducer)

Parameters

  • type (string): Will be prefixed to every action-type
  • initIdentityReducer (function): transforms multiple arguments into an object which will be merged with the resulting action.

Returns:

[
  initAction, // function(...args) => { type: `${type}/init`, ... initIdentityReducer(...args) },
  resolveAction, // function(payload) => { type: `${type}/resolve`, payload }
  rejectAction // function(error) => { type: `${type}/resolve`, error }
]

Usage

let {
  fetch,
  fetchSuccess,
  fetchFail
} = createResolvableActions('fetch', (userId) => ({ userId }))
// fetch.toString() === 'fetch/init'
// fetch(10) => { type: 'fetch/init', userId: 10 }  
// fetchSuccess.toString() === 'fetch/resolve'  
// fetchFail.toString() === 'fetch/reject'
createResolvableActions(type, actionMap)

Parameters

  • type (string): Will be prefixed to every action-type
  • actionMap (object): Object which keys are used as action-types and values are used as identityReducers.

Returns

[
  initAction, // function(...args) => { type: `${type}/init`, ...actionMap.init(...args) },
  resolveAction, // function(...args) => { type: `${type}/resolve`, ...actionMap.resolve(...args) }
  rejectAction // function(...args) => { type: `${type}/resolve`, ...actionMap.reject(...args) }
]

Usage

let {
  fetch,
  fetchSuccess,
  fetchFail
} = createResolvableActions('fetch', {
  init: (userId) => ({ userId }),
  resolve: (userId, user) => ({ userId, user }),
  reject: (userId, error) => ({ userId, error })
})
// fetch.toString() === 'fetch/init'  
// fetch(10) => { type: 'fetch/init', userId: 10 }
// fetchSuccess.toString() === 'fetch/resolve'
// fetchSuccess(10, {}) => { type: 'fetch/init', userId: 10, user: {} }  
// fetchFail.toString() === 'fetch/reject'  
// fetchFail(10, 'oh no') => { type: 'fetch/init', userId: 10, error: 'oh no' }

createResolvableActionsDomain(domain)

Works like createActionsDomain. Returns a domain-prefixed createResolvableActions. Usefull if you need to create multiple actions, scoped on a domain.

Parameters

  • domain (string): Will be prefixed to every action type

Usage

const createResolvableUserActions = createResolvableActionsDomain('users')

const [
  fetchUsers,
  fetchUsersSucces,
  fetchUsersFail
] = createResolvableUserActions('fetch')
// fetchUsers.toString() === 'users//fetch/init'  
// fetchUsersSuccess.toString() === 'users//fetch/resolve'  
// fetchUsersFail.toString() === 'users//fetch/reject'  

Readme

Keywords

none

Package Sidebar

Install

npm i @touchtribe/redux-helpers

Weekly Downloads

369

Version

2.0.1

License

ISC

Unpacked Size

99.2 kB

Total Files

32

Last publish

Collaborators

  • doxick
  • mnjongerius