@touchtribe/redux-helpers
v2.0.1
Published
Simple helpers to use with redux and react
Downloads
542
Readme
@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
asreducer.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 dispatchasyncResolveAction(...args)
and then resolve the Promise with that actionaction.reject(...args)
will dispatchasyncRejectAction(...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'