Reduce boilerplate code in your Redux action creators and types with support for normalizr, universal JavaScript, and Redux-first Router.
yarn add redux-action-creator
Use createTypes(types, [namespace])
to create action types with far less boilerplate.
types
is an array of strings designating the name of the action types and namespace
is an optional prefix to prevent name collisions.
Before:
const types = {
CREATE_CAR: 'CAR_CREATE_CAR',
EDIT_CAR: 'CAR_EDIT_CAR',
EDIT_WHEELS: 'CAR_EDIT_WHEELS'
};
After:
import {createTypes} from 'redux-action-creator';
const types = createTypes(['CREATE_CAR', 'EDIT_CAR', 'EDIT_WHEELS'], 'CAR');
Asynchronous actions typically come in sets of three with the main action type along with a success and fail type.
Use the async(type)
helper to generate this set.
Before:
const types = {
LOAD_CARS: 'CAR_LOAD_CARS',
LOAD_CARS_SUCCESS: 'CAR_LOAD_CARS_SUCCESS',
LOAD_CARS_FAIL: 'CAR_LOAD_CARS_FAIL',
ADD_CAR: 'CAR_ADD_CAR',
ADD_CAR_SUCCESS: 'CAR_ADD_CAR_SUCCESS',
ADD_CAR_FAIL: 'CAR_ADD_CAR_FAIL'
};
After:
import {createTypes, async} from 'redux-action-creator';
// use ES6 spread syntax to succinctly merge the returned asynchronous types
const types = createTypes([
...async('LOAD_CARS'),
...async('ADD_CAR')
], 'CAR');
Synchronous action creators can be defined using the actionCreator(type [, propName1, propName2, ..., propNameX])
helper.
This will return a function that accepts the list of params as arguments and returns an action with the given type and arguments payload.
Before:
var actions = {
createCar: function() {
return {type: 'CAR_CREATE_CAR'};
},
editCar: function({car}) {
return {type: 'CAR_EDIT_CAR', payload: {
car: car
}};
}
};
After:
import {actionCreator} from 'redux-action-creator';
const actions = {
createCar: actionCreator(types.CREATE_CAR),
editCar: actionCreator(types.EDIT_CAR, 'car')
};
Asynchronous action creators can be defined using the asyncActionCreator(type [, propName1, propName2, ..., propNameX], action|config)
helper.
If a function is passed as the last parameter, it will be treated as the asynchronous action that must return a promise, otherwise it can be a configuration object that accepts the following values:
action
: an asynchronous, promise-returning actionschema
: a normalizr schema which will parse the response before returning- Note: see Universal usage below for more configuration options
The action given to asyncActionCreator
will eventually be called with the payload, any helpers
(see Helpers below) passed in, including the dispatch
function, and whatever
extra arguments as determined by the async middleware and the configuration of it. For example, if used with
redux-thunk, the action will be called with the payload, the helpers, the getState
function, and the extraArgument
if the middleware was created via the withExtraArgument
function.
Note: All other properties provided in config
apart from action
, client
, server
, and schema
will be appended
to all actions dispatched from the action creator enabling further customisation if needed.
Before:
var actions = {
loadCars: function() {
return function(dispatch) {
dispatch({type: 'CAR_LOAD_CARS'});
return get('/cars').then(function(response) {
dispatch({type: 'CAR_LOAD_CARS_SUCCESS', response: response});
}).catch(function(err) {
dispatch({type: 'CAR_LOAD_CARS_FAIL', error: {message: err.message, code: err.code}});
});
};
},
addCar: function({electric, wheels}) {
return function(dispatch) {
var payload = {electric: electric, wheels: wheels};
dispatch({type: 'CAR_ADD_CAR', payload: payload});
return post('/cars', payload).then(function(response) {
dispatch({
type: 'CAR_ADD_CAR_SUCCESS',
payload: payload,
response: normalize(response, new Schema('cars'))
});
}).catch(function(err) {
dispatch({type: 'CAR_ADD_CAR_FAIL', payload: payload, error: {message: err.message, code: err.code}});
});
};
}
};
After:
import {Schema} from 'normalizr';
import {asyncActionCreator} from 'redux-action-creator';
const actions = {
loadCars: asyncActionCreator(types.LOAD_CARS, () => get('/cars')),
addCar: asyncActionCreator(types.ADD_CAR, 'electric', 'wheels', {
action: payload => post('/cars', payload),
schema: new Schema('cars')
})
};
The action
returned by asyncActionCreator
accepts a payload object and also a helpers
object. This allows you to pass in any further functions required by the asynchronous action. For example, if using a library to help with forms such as reformist, quite often you'll require some form state to be modified depending on the state of the asynchronous action. Libraries such as reformist
provide functions to handle this such as setSubmitting
and setErrors
. These functions can be passed through as helpers to the asynchronous action to be used when the asynchronous task fails or succeeds. The dispatch
function will also be included in the helpers
object.
Instead of passing a single action to the asyncActionCreator
, you can instead pass a client
action and a
server
action and the appropriate function will be executed depending on the context in which it is run.
const actions = {
loadCars: asyncActionCreator(types.LOAD_CARS, {
client: () => get('/cars'),
server: () => carService.loadCars(),
schema: new Schema('cars')
})
};
In this example, the client
action will be executed if run in the browser whereas the server
action will run on
the server when using a library such as redux-connect.
Note: if you have server-side only dependencies in your server
action that will create a troublesome client webpack build,
use the custom webpack universal-action-creator-loader to strip
the server
action from the action creator. All server-side dependencies must be require
d from within the server
function.
Asynchronous routes for Redux-first Router can be defined using the asyncRoute(type, path, action|config, [helpers])
helper.
It works similar to the asyncActionCreator
described above with the addition of the route path
and the helpers
object
containing any helper utilities the action may need to route correctly. The action given to asyncRoute
will eventually be
called with the payload, the dispatch
function, the getState
function, and finally the helpers
object if given.
asyncRoute
will return a route object of the form
{
[type]: {
path,
thunk,
...rest
}
}
where rest
is any further properties of config
apart from action
, client
, server
, and schema
that you need to
define the route.
For example, this can be used to set an isSecure
property on the route to signify that the route requires authorisation.
The returned route object can then be merged into the final routes map to be passed to Redux-first Router's connectRoutes
function.
const routesMap = {
ROUTES_HOME: '/home',
...asyncRoute('ROUTES_CARS', '/cars', () => get('/cars')),
...asyncRoute('ROUTES_CAR', '/cars/:id', payload => get(`/cars/${payload.id}`))
}
Use createRouteTypes(types)
as a shortcut to creating types with 'ROUTES' as the namespace.