Ducks: Redux Reducer Bundles
I find as I am building my redux app, one piece of functionality at a time, I keep needing to add {actionTypes, actions, reducer}
tuples for each use case. I have been keeping these in separate files and even separate folders, however 95% of the time, it's only one reducer/actions pair that ever needs their associated actions.
To me, it makes more sense for these pieces to be bundled together in an isolated module that is self contained, and can even be packaged easily into a library.
The Proposal
Example
See also: Common JS Example.
// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action Creators
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
return dispatch => get('/widget').then(widget => dispatch(setWidget(widget)))
}
Rules
A module...
- MUST
export default
a function calledreducer()
- MUST
export
its action creators as functions - MUST have action types in the form
npm-module-or-app/reducer/ACTION_TYPE
- MAY export its action types as
UPPER_SNAKE_CASE
, if an external reducer needs to listen for them, or if it is a published reusable library
These same guidelines are recommended for {actionType, action, reducer}
bundles that are shared as reusable Redux libraries.
Name
Java has jars and beans. Ruby has gems. I suggest we call these reducer bundles "ducks", as in the last syllable of "redux".
Usage
You can still do:
import { combineReducers } from 'redux';
import * as reducers from './ducks/index';
const rootReducer = combineReducers(reducers);
export default rootReducer;
You can still do:
import * as widgetActions from './ducks/widgets';
...and it will only import the action creators, ready to be passed to bindActionCreators()
.
There will be some times when you want to export
something other than an action creator. That's okay, too. The rules don't say that you can only export
action creators. When that happens, you'll just have to enumerate the action creators that you want. Not a big deal.
import {loadWidgets, createWidget, updateWidget, removeWidget} from './ducks/widgets';
// ...
bindActionCreators({loadWidgets, createWidget, updateWidget, removeWidget}, dispatch);
Example
React Redux Universal Hot Example uses ducks. See /src/redux/modules
.
Implementation
The migration to this code structure was painless, and I foresee it reducing much future development misery.
Although it's completely feisable to implement it without any extra library, there are some tools that might help you:
- extensible-duck - Implementation of the Ducks proposal. With this library you can create reusable and extensible ducks.
- redux-duck - Helper function to create Redux modules using the ducks-modular-redux proposal
- modular-redux-thunk - A ducks-inspired package to help organize actions, reducers, and selectors together - with built-in redux-thunk support for async actions.
- molecular-js - Set of utilities to ease the development of modular state management patterns with Redux (also known as ducks).
Please submit any feedback via an issue or a tweet to @erikras. It will be much appreciated.
Happy coding!
-- Erik Rasmussen
Translation
Photo credit to Airwolfhound.