Redux Tools to make redux easy and reduce boilerplate with useful tools and conventions.
Please view the example folder to get a quick introduction or continue reading.
Does this folder structure look familiar to you?
src/
actions/
index.js
products.js
user.js
components/
App.js
Header.js
Product.js
reducers/
index.js
products.js
user.js
selectors/
index.js
products.js
user.js
types/
index.js
products.js
user.js
index.js
store.js
This is the basic organization shown in many different redux tutorials and examples. This works great until, one day, it doesn't. It becomes cumbersome to make changes across multiple folders when you're only really updating one thing. This is where models come in.
The solution is to place things together by domain rather than type. All fetching, updating, and calculating for a domain lives inside one model. This includes actions, types, reducers, selectors, sagas. So that example app structure using models would look like this:
src/
components/
App.js
Header.js
Product.js
models/
products/
actions.js
index.js
reducer.js
sagas.js
selectors.js
user/
actions.js
index.js
reducer.js
sagas.js
selectors.js
index.js
index.js
store.js
A model can be a folder with separate files for actions, reducer, selectors, etc. Or for simple models just a single file may be all you need (similar to Ducks in many ways). At the end of the day it will be an object with one or several of the following keys (each piece is optional):
- actions
- an object where each key is an action creator function
- reducer
- a reducer function for a branch of the state object, usually with the same name as the model itself
- sagas
- a single saga function that will be forked by the root reducer
- selectors
- an object of selector functions related to the model
- types
- an object of
SNAKE_CASEkeys andnamespace/SNAKE_CASEvalues for use in reducers and sagas (and anywhere else you need them)
- an object of
Next let's look at each piece in a little more detail.
This is what a typical definition for a type and action looks like:
const SET_FIRST_NAME = 'user/SET_FIRST_NAME';
const setFirstName = (payload) => ({
payload,
type: SET_FIRST_NAME,
});And that gets even more repetitive when you start dealing with asynchronous actions:
const SAVE_REQUEST = 'user/SAVE_REQUEST';
const SAVE_SUCCESS = 'user/SAVE_SUCCESS';
const SAVE_FAILURE = 'user/SAVE_FAILURE';
const saveRequest = (payload) => ({
payload,
type: SAVE_REQUEST,
});
const saveSuccess = (payload) => ({
payload,
type: SAVE_SUCCESS,
});
const saveFailure = (payload) => ({
payload,
type: SAVE_FAILURE,
});That repetition was a source of great angst for us so we thought there's a great opportunity for convention and tooling to reduce boilerplate.
The first step was to decide that since actions and types are so closely related anyway, why don't we explicitly couple them together?
- actions have a
typeand apayload. Action creators will always accept a single parameter that will be placed onto thepayloadkey of the action object. - types will be
SNAKE_CASEwith anamespaceof any case - action creators will have the
camelCaseversion of the type'sSNAKE_CASEvalue. - action creators will be generated automatically from types
So using our conventions and adding in redux-tools tooling, we can greatly reduce the boilerplate for creating actions and types. Here's an equivalent of the above two examples:
import { Async, Basic } from 'redux-tools/types';
import { generateActionsAndTypes } from 'redux-tools';
const { actions, types } = generateActionsAndTypes('user', [
new Async('SAVE'),
new Basic('SET_FIRST_NAME'),
new Basic('SET_LAST_NAME'),
]);Model reducers are combined into the root reducer using Redux's combineReducers function.
Model sagas are combined by forking each saga inside the root saga.
Use an implementation of the Type class when defining your types. Redux Tools comes with two types defined out of the box: Async and Basic.
When used with generateActionsAndTypes, this will result in a namespaced type and action creator.
import { Basic } from 'redux-tools/types';
import { generateActionsAndTypes } from 'redux-tools';
const { actions, types } = generateActionsAndTypes('user', [new Basic('SET_FIRST_NAME')]);
console.log(actions);
// { setFirstName: (payload) => ({ payload, type: 'user/SET_FIRST_NAME' }) }
console.log(types);
// { SET_FIRST_NAME: 'user/SET_FIRST_NAME' }When used with generateActionsAndTypes, this will result in request, success, and failure action creators and types.
import { Async } from 'redux-tools/types';
import { generateActionsAndTypes } from 'redux-tools';
const { actions, types } = generateActionsAndTypes('user', [new Async('SAVE')]);
console.log(actions);
/*
{
saveRequest: (payload) => ({ payload, type: 'user/SAVE_REQUEST' }),
saveSuccess: (payload) => ({ payload, type: 'user/SAVE_SUCCESS' }),
saveFailure: (payload) => ({ payload, type: 'user/SAVE_FAILURE' }),
}
*/
console.log(types);
/*
types: {
SAVE_REQUEST: 'user/SAVE_REQUEST',
SAVE_SUCCESS: 'user/SAVE_SUCCESS',
SAVE_FAILURE: 'user/SAVE_FAILURE',
}
*/namespace(string)types(array)
{
actions: {},
types: {},
}import { Async, Basic } from 'redux-tools/types';
import { generateActionsAndTypes } from 'redux-tools';
const { actions, types } = generateActionsAndTypes('user', [
new Async('SAVE'),
new Basic('SET_FIRST_NAME'),
new Basic('SET_LAST_NAME'),
]);
console.log(actions);
/*
{
saveRequest: (payload) => ({ payload, type: 'user/SAVE_REQUEST' }),
saveSuccess: (payload) => ({ payload, type: 'user/SAVE_SUCCESS' }),
saveFailure: (payload) => ({ payload, type: 'user/SAVE_FAILURE' }),
setFirstName: (payload) => ({ payload, type: 'user/SET_FIRST_NAME' }),
setLastName: (payload) => ({ payload, type: 'user/SET_LAST_NAME' }),
}
*/
console.log(types);
/*
types: {
SAVE_REQUEST: 'user/SAVE_REQUEST',
SAVE_SUCCESS: 'user/SAVE_SUCCESS',
SAVE_FAILURE: 'user/SAVE_FAILURE',
SET_FIRST_NAME: 'user/SET_FIRST_NAME',
SET_LAST_NAME: 'user/SET_LAST_NAME',
}
*/Function to combine your different models for use in your app. Typically used in the index.js file in your models directory.
models(object)
{
actions,
reducer,
sagas,
selectors,
types,
}import app from './app';
import lists from './lists';
import { models } from 'redux-tools';
import todos from './todos';
export const { actions, reducer, sagas, selectors, types } = models.create({
app,
lists,
todos,
});
console.log(actions);
/*
{
app: { ... },
lists: { ... },
todos: { ... },
}
*/
// Root reducer function to use in your redux store
console.log(reducer);
// Root saga generator function to pass to the saga middleware
console.log(sagas);
console.log(selectors);
/*
{
app: { ... },
lists: { ... },
todos: { ... },
}
*/
console.log(types);
/*
{
app: { ... },
lists: { ... },
todos: { ... },
}
*/withErrorReporting: extend to allow raygun / crashalitics / error reporting system withRetry: extend to allow raygun / crashalitics / error reporting system