Redux Helpers
This library aims to provide a set of simple, easy to use factory methods to help reduce the boilerplate of writing Redux reducers and action creators.
Go to live examples, code and docs!
Getting Started
Install with Yarn
yarn add @customd/cd-redux-helpers
Or with NPM
npm install --save @customd/cd-redux-helpers
Reducers
Implement a full reducer managing byId
, ids
, loading
and error
state keys:
import { createReducers } from '@customd/cd-redux-helpers'
export default createReducers('articles', ['byId', 'ids', 'loading', 'error'])
export const getArticles = (state) => Object.values(state.byId)
export const getArticleIds = (state) => state.ids
export const getArticlesById = (state) => state.byId
export const getArticleById = (state, id) => state.byId[id]
export const getArticleLoading = (state) => state.loading
export const getArticleError = (state) => state.error
Available reducers are as follows:
Reducer Name | Description |
---|---|
byId |
An object of objects, arranged against id key. |
ids |
An array of IDs which maintains order of items as passed in the action |
loading |
Boolean which tells you whether it's loading or not |
saving |
Boolean which tells you whether it's saving or not |
error |
An error key, typically set to the thrown error object |
Pick-n-mix implementation
This example implements a fully featured reducer set which is functionally identical to the createReducer
example above, except adds an additional custom reducer.
import { combineReducers } from 'redux'
import {
createReducerById,
createReducerIds,
createReducerLoading,
createReducerError
} from '@customd/cd-redux-helpers/dist/reducers'
const byId = createReducerById('articles')
const ids = createReducerIds('articles')
const loading = createReducerLoading('articles')
const error = createReducerError('articles')
const customReducer = (state = false, action) => {
switch(action.type) {
case 'CUSTOM_ARTICLES_ACTION':
return true
default:
return state
}
}
export default combineReducers({
byId,
ids,
loading,
error,
customReducer
})
export const getArticles = (state) => Object.values(state.byId)
export const getArticleIds = (state) => state.ids
export const getArticlesById = (state) => state.byId
export const getArticleById = (state, id) => state.byId[id]
export const getArticleLoading = (state) => state.loading
export const getArticleError = (state) => state.error
Composition
These methods are composable. Suppose you want to handle an additional action CLEAR_ARTICLES_IDS
in the ids
reducer.
import { createReducerIds } from '@customd/cd-redux-helpers/dist/reducers'
const reduceIds = createReducerIds('articles')
const ids = (state = [], action) => {
if( action.type === 'CLEAR_ARTICLES_IDS') {
return []
}
return reduceIds(state, action)
}
export default ids
Action Creators
You can also use this package to provide re-usable and composable action creators, including async / redux-thunk
action creators.
Basic action creators
Available 'basic' action creator factory methods — these factories will create simple action creators which return actions suitable for use with the reducers generated by this package.
Factory Method | Type | Description |
---|---|---|
createSetAction | basic | A SET_X action creator |
createAddAction | basic | An ADD_X action creator |
createUpdateAction | basic | An UPDATE_X action creator |
createLoadingAction | basic | A SET_X_LOADING action creator |
createSavingAction | basic | A SET_X_SAVING action creator |
createErrorAction | basic | A SET_X_ERROR action creator |
Each of the below factory methods accepts a single argument which serves as the action name.
E.g.,
const setArticles = createSetAction('articles')
setArticles([{ id: 1, title: 'foo' }])
And implement a full set of actions to work with the above reducers:
import {
createSetAction,
createAddAction,
createUpdateAction,
createLoadingAction,
createSavingAction,
createErrorAction
} from '@customd/cd-redux-helpers/dist/actions'
export const setArticles = createSetAction('articles')
export const addArticles = createAddAction('articles')
export const updateArticles = createUpdateAction('articles')
export const setArticlesLoading = createLoadingAction('articles')
export const setArticlesSaving = createSavingAction('articles')
export const setArticlesError = createErrorAction('articles')
Limit action update scope
Let's say you want an action which sets a single blog article for use elsewhere in the site, and an action which bulk-sets articles to display in a list. You don't want your first article to appear in the list, as it may not be relevant, however because you're DRY and you want to take best advantage of Redux by opportunistically re-using your pre-loaded articles, you decide you want all articles stored in the same state.blog.articles.byId
state.
You can use @customd/cd-redux-helpers
scope limiting updaters, to change the action you fire so it only applies to the byId
state, and leaves the ids
state alone (or vice versa).
import {
createUpdateAction,
SET_BY_ID_ONLY
} from '@customd/cd-redux-helpers/dist/actions'
export const updateArticlesDetail = createUpdateAction('articles', SET_BY_ID_ONLY)
This has the effect of changing the action you're firing from UPDATE_ARTICLES
to UPDATE_ARTICLES_BY_ID
. This action works exactly the same on the byId
state, but is ignored by the ids
state entirely.
Async action creators
There are also factory methods for async
action creators (intended for use with redux-thunk
middleware).
createFetchAction
This method makes an async action creator which is aimed at returning an ordered set of IDs matching a request. Ideally for large data sets this would return only an array of IDs, though for smaller data-sets, you may also like to return a flag field, or a small amount of content for client side filtering, and for very small data sets you may simply return all the data you need in this single request.
import Blog from 'api/Blog'
import {
createFetchAction,
createUpdateAction,
} from '@customd/cd-redux-helpers/dist/actions'
export const updateArticles = createUpdateAction('articles')
export const fetchArticles = createFetchAction(
Blog.getWhere,
updateArticles
)
Often you'll want to provide a default set of arguments to the API query:
import Blog from 'api/Blog'
import {
createFetchAction,
createUpdateAction,
} from '@customd/cd-redux-helpers/dist/actions'
export const updateArticles = createUpdateAction('articles')
export const fetchArticles = createFetchAction(
[Blog.getWhere, { fields: 'id', status: 'published' }, '-published'],
updateArticles
)
A more advanced implementation tracking loading & errors:
import Blog from 'api/Blog'
import {
createFetchAction,
createUpdateAction,
createLoadingAction,
createErrorAction
} from '@customd/cd-redux-helpers/dist/actions'
export const updateArticles = createUpdateAction('articles')
export const setArticlesLoading = createLoadingAction('articles')
export const setArticlesError = createErrorAction('articles')
export const fetchArticles = createFetchAction(
[Blog.getWhere, { fields: 'id', status: 'published' }, '-published'],
updateArticles,
{
loadingAction : setArticlesLoading,
errorAction : setArticlesError
}
)
Action creator usage
Somewhere in your connected component, make the call. Note that you can override the default Blog.getWhere
arguments when calling.
const { fetchArticles, authorId } = this.props
fetchArticles({ fields: 'id,title,published' })
Arguments
Position | Type | Description |
---|---|---|
1 |
Array or Callable
|
A Callable which returns a Promise , or an array, where the remaining array items are passed to the Callable as a default set of arguments. Promise should return an object with minimum { result: 'success', data: '...' } , or { result: 'error', error: '...' }
|
2 | Callable |
A Callable which returns an action creator (can be either a thunk action, or a vanilla action). This Callable recieves the response.data key (or whatever response key as defined in the responseData option). |
3 | Object |
A configuration object, with options as described below. |
Options
Option | Default | Description |
---|---|---|
loadingAction | - | The action to fire when loading starts |
errorAction | - | The action to fire if we encounter an arror in the fetch request |
responseResult | result | The property name of the response to use when checking for a response result success |
responseData | data | The property name for the data to pass to the update action from the response object. Pass ACTION_RETURN_FULL_RESPONSE constant to return the full response object. |
responseError | error | The property name for the error content used to throw Error when responseResult key is not === 'success' . Pass ACTION_RETURN_FULL_RESPONSE constant to throw the full response instead of an Error . |
createFetchDetailAction
This method makes an async action creator which is aimed at fetching record detail from an API by record ID. The purpose is to feed in an array of IDs which will be retrieved.
import Blog from 'api/Blog'
import {
createFetchDetailAction,
createUpdateAction,
} from '@customd/cd-redux-helpers/dist/actions'
export const updateArticles = createUpdateAction('articles')
export const fetchArticlesDetail = createFetchDetailAction(
[Blog.getWith, { status: 'published' }, ['authors', 'tags']],
updateArticles
)
Action creator usage
Somewhere in your connected component, make the call. Note that you can override the default Blog.getWith
arguments when calling.
const { fetchArticlesDetail, authorId } = this.props
fetchArticlesDetail({ authorId }, ['authors', 'tags', 'categories', 'comments'])
Arguments
Position | Type | Description |
---|---|---|
1 |
Array or Callable
|
A Callable which returns a Promise , or an array, where the remaining array items are passed to the Callable as a default set of arguments. Promise should return an object with minimum { result: 'success', data: '...' } , or { result: 'error', error: '...' }
|
2 | Callable |
A Callable which returns an action creator (can be either a thunk action, or a vanilla action). This Callable recieves the response.data key (or whatever response key as defined in the responseData option). |
3 | Object |
A configuration object, with options as described below. |
Options
Option | Default | Description |
---|---|---|
loadingAction | - | The action to fire when loading starts |
errorAction | - | The action to fire if we encounter an arror in the fetch request |
responseResult | 'result' |
The property name of the response to use when checking for a response result success |
responseData | 'data' |
The property name for the data to pass to the update action from the response object. Pass ACTION_RETURN_FULL_RESPONSE constant to return the full response object. |
responseError | 'error' |
The property name for the error content used to throw Error when responseResult key is not === 'success' . Pass ACTION_RETURN_FULL_RESPONSE constant to throw the full response instead of an Error . |
chunkSize | 500 |
Chunks request IDs up into groups of no larger than this value. Pass ACTION_DONT_CHUNK constant to stop this method from chunking requests. |
Custom response manipulation
Let's say you need to adjust some content before you update the articles. Since we're just passing plain methods around, you can compose the updateArticles
function call;
import {
createUpdateAction,
createSetAction,
createFetchAction,
ACTION_RETURN_FULL_RESPONSE
} from '@customd/cd-redux-helpers/dist/actions'
export const updateArticles = createUpdateAction('articles')
export const setArticleCount = createSetAction('articles_count')
export const fetchArticles = createFetchAction([ Blog.getWhere,
{ status: 'published' },
'-published'
],
(response) => {
const nextArticles = response.articles.map(({ id, title, abstract }) =>({
id,
title: title.trim()
abstract: abstract.trim()
}))
return (dispatch, getState) => {
dispatch(updateArticles(nextArticles))
dispatch(setArticleCount(response.pagination.count))
}
},
{
loadingAction: setArticlesLoading,
errorAction: setArticlesError,
responseData: ACTION_RETURN_FULL_RESPONSE
}
)
The same concept applies to Loading and Error actions.
Contributing
If you find a bug, error or feature that you wish to fix/implement and contribute back to the project, you can do so by committing your work to a new branch, and issuing a merge request.
Once you've created your branch, push it to the repository, and then issue a merge request targeting develop
or a release
branch.
Make sure you leave a note about why this fix is important, how you found the solution, and any implications this solution might have. Use the merge request template provided.
GitLab will automatically send out an email to the maintainer — that person will then be able to review, test and ensure the change is documented.
The person assigned to the merge will:
- Test the merge request against the
develop
branch. - Document the modifications.
- Publish a new release if required, or add to the next release.
- Communicate with the developer who raised the request, and work out if the change needs to be implemented as a hotfix update for earlier major versions.
- Ensure tests have been written, and pass.
- Ensure new features or behaviours have been documented.
Publishing
Ensure you're logged in and registered to npm.customd.com before publishing.
- Commit and push all changes to a
release/xyz
branch - Run
yarn publish
- Merge and squash changes in to
master