├── CommonJs.md
├── README.md
├── battlecry
└── generators
│ └── duck
│ ├── duck.generator.js
│ └── templates
│ ├── __naMe__.js
│ └── configureStore.js
├── duck.jpg
└── migrate.jpg
/CommonJs.md:
--------------------------------------------------------------------------------
1 | ## Common JS Example
2 |
3 | ```javascript
4 | // widgets.js
5 |
6 | const LOAD = 'my-app/widgets/LOAD';
7 | const CREATE = 'my-app/widgets/CREATE';
8 | const UPDATE = 'my-app/widgets/UPDATE';
9 | const REMOVE = 'my-app/widgets/REMOVE';
10 |
11 | function reducer(state = {}, action = {}) {
12 | switch (action.type) {
13 | // do reducer stuff
14 | default: return state;
15 | }
16 | }
17 |
18 | reducer.loadWidgets = function() {
19 | return { type: LOAD };
20 | }
21 |
22 | reducer.createWidget = function(widget) {
23 | return { type: CREATE, widget };
24 | }
25 |
26 | reducer.updateWidget = function(widget) {
27 | return { type: UPDATE, widget };
28 | }
29 |
30 | reducer.removeWidget = function(widget) {
31 | return { type: REMOVE, widget };
32 | }
33 |
34 | module.exports = reducer;
35 | ```
36 |
37 |
38 | One of the different caveats is that you can't use Redux' `bindActionCreators()` directly with a duck module, as it assumes that when given a function, it's a single action creator, so you need to do something like:
39 |
40 | ```javascript
41 | var actionCreators = require('./ducks/widgets');
42 | bindActionCreators({ ...actionCreators });
43 | ```
44 |
45 | Another is that if you're also exporting some type constants, you need to attach those to the reducer function too, so you can't unpack just the action creators into another object at import time as easily (no `as` syntax) so the above trick isn't as viable.
46 |
47 | You can avoid getting bitten by both of these by rolling your own dispatch binding function - this is the one I'm using to create a function to be passed as the `mapDispatchToProps` argument to `connect()`:
48 |
49 | ```javascript
50 | /**
51 | * Creates a function which creates same-named action dispatchers from an object
52 | * whose function properties are action creators. Any non-functions in the actionCreators
53 | * object are ignored.
54 | */
55 | var createActionDispatchers = actionCreators => dispatch =>
56 | Object.keys(actionCreators).reduce((actionDispatchers, name) => {
57 | var actionCreator = actionCreators[name];
58 | if (typeof actionCreator == 'function') {
59 | actionDispatchers[name] = (...args) => dispatch(actionCreator(...args));
60 | }
61 | return actionDispatchers;
62 | }, {})
63 |
64 | var actionCreators = require('./ducks/widgets');
65 | var mapStateToProps = state => state.widgets;
66 | var mapDispatchToProps = createActionDispatchers(actionCreators);
67 |
68 | var MyComponent = React.createClass({ /* ... */ });
69 |
70 | module.exports = connect(mapStateToProps , mapDispatchToProps)(MyComponent);
71 | ```
72 |
73 | ---
74 | This document copied almost verbatim from [@insin](https://github.com/insin)'s issue [#2](https://github.com/erikras/ducks-modular-redux/issues/2).
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # Ducks: Redux Reducer Bundles
8 |
9 |
10 |
11 | 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.
12 |
13 | 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.
14 |
15 | ## The Proposal
16 |
17 | ### Example
18 |
19 | See also: [Common JS Example](CommonJs.md).
20 |
21 | ```javascript
22 | // widgets.js
23 |
24 | // Actions
25 | const LOAD = 'my-app/widgets/LOAD';
26 | const CREATE = 'my-app/widgets/CREATE';
27 | const UPDATE = 'my-app/widgets/UPDATE';
28 | const REMOVE = 'my-app/widgets/REMOVE';
29 |
30 | // Reducer
31 | export default function reducer(state = {}, action = {}) {
32 | switch (action.type) {
33 | // do reducer stuff
34 | default: return state;
35 | }
36 | }
37 |
38 | // Action Creators
39 | export function loadWidgets() {
40 | return { type: LOAD };
41 | }
42 |
43 | export function createWidget(widget) {
44 | return { type: CREATE, widget };
45 | }
46 |
47 | export function updateWidget(widget) {
48 | return { type: UPDATE, widget };
49 | }
50 |
51 | export function removeWidget(widget) {
52 | return { type: REMOVE, widget };
53 | }
54 |
55 | // side effects, only as applicable
56 | // e.g. thunks, epics, etc
57 | export function getWidget () {
58 | return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
59 | }
60 |
61 | ```
62 | ### Rules
63 |
64 | A module...
65 |
66 | 1. MUST `export default` a function called `reducer()`
67 | 2. MUST `export` its action creators as functions
68 | 3. MUST have action types in the form `npm-module-or-app/reducer/ACTION_TYPE`
69 | 3. 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
70 |
71 | These same guidelines are recommended for `{actionType, action, reducer}` bundles that are shared as reusable Redux libraries.
72 |
73 | ### Name
74 |
75 | Java has jars and beans. Ruby has gems. I suggest we call these reducer bundles "ducks", as in the last syllable of "redux".
76 |
77 | ### Usage
78 |
79 | You can still do:
80 |
81 | ```javascript
82 | import { combineReducers } from 'redux';
83 | import * as reducers from './ducks/index';
84 |
85 | const rootReducer = combineReducers(reducers);
86 | export default rootReducer;
87 | ```
88 |
89 | You can still do:
90 |
91 | ```javascript
92 | import * as widgetActions from './ducks/widgets';
93 | ```
94 | ...and it will only import the action creators, ready to be passed to `bindActionCreators()`.
95 |
96 | > Actually, it'll also import `default`, which will be the reducer function. It'll add an action creator named `default` that won't work. If that's a problem for you, you should enumerate each action creator when importing.
97 |
98 | 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.
99 |
100 | ```javascript
101 | import {loadWidgets, createWidget, updateWidget, removeWidget} from './ducks/widgets';
102 | // ...
103 | bindActionCreators({loadWidgets, createWidget, updateWidget, removeWidget}, dispatch);
104 | ```
105 |
106 | ### Example
107 |
108 | [React Redux Universal Hot Example](https://github.com/erikras/react-redux-universal-hot-example) uses ducks. See [`/src/redux/modules`](https://github.com/erikras/react-redux-universal-hot-example/tree/master/src/redux/modules).
109 |
110 | [Todomvc using ducks.](https://github.com/goopscoop/ga-react-tutorial/tree/6-reduxActionsAndReducers)
111 |
112 | ### BattleCry generators
113 |
114 | There are configurable [BattleCry](https://github.com/pedsmoreira/battlecry) generators ready to be downloaded and help scaffolding ducks:
115 |
116 | ```sh
117 | npm install -g battlecry
118 | cry download generator erikras/ducks-modular-redux
119 | cry init duck
120 | ```
121 |
122 | Run `cry --help` to check more info about the generators available;
123 |
124 | ### Implementation
125 |
126 | The migration to this code structure was [painless](https://github.com/erikras/react-redux-universal-hot-example/commit/3fdf194683abb7c40f3cb7969fd1f8aa6a4f9c57), and I foresee it reducing much future development misery.
127 |
128 | Although it's completely feasable to implement it without any extra library, there are some tools that might help you:
129 |
130 | * [extensible-duck](https://github.com/investtools/extensible-duck) - Implementation of the Ducks proposal. With this library you can create reusable and extensible ducks.
131 | * [saga-duck](https://github.com/cyrilluce/saga-duck) - Implementation of the Ducks proposal in Typescript with [sagas](https://github.com/redux-saga/redux-saga) in mind. Results in reusable and extensible ducks.
132 | * [redux-duck](https://github.com/PlatziDev/redux-duck) - Helper function to create Redux modules using the ducks-modular-redux proposal
133 | * [modular-redux-thunk](https://github.com/benbeadle/modular-redux-thunk) - A ducks-inspired package to help organize actions, reducers, and selectors together - with built-in redux-thunk support for async actions.
134 | * [molecular-js](https://www.npmjs.com/package/molecular-js) - Set of utilities to ease the development of modular state management patterns with Redux (also known as ducks).
135 | * [ducks-reducer](https://github.com/drpicox/ducks-reducer) - Function to combine _ducks object_ reducers into one reducer (equivalent to [combineReducers](https://redux.js.org/docs/api/combineReducers.html)), and function [ducks-middleware](https://github.com/drpicox/ducks-middleware) to combine _ducks object_ middleware into one single middleware compatible with [applyMiddleware](https://redux.js.org/docs/api/applyMiddleware.html).
136 | * [simple-duck](https://github.com/xander27/simple-duck) - Class based implementation of modules system, inspired by ducks-modular-redux. All OOP benefits like inheritance and composition. Support combining of duck-module classes and regular reducer functions using `combineModules` function.
137 |
138 | Please submit any feedback via an issue or a tweet to [@erikras](https://twitter.com/erikras). It will be much appreciated.
139 |
140 | Happy coding!
141 |
142 | -- Erik Rasmussen
143 |
144 |
145 | ### Translation
146 |
147 | [한국어](https://github.com/JisuPark/ducks-modular-redux)
148 | [中文](https://github.com/deadivan/ducks-modular-redux)
149 | [Türkçe](https://github.com/mfyz/ducks-modular-redux-tr)
150 |
151 | ---
152 |
153 | 
154 | > Photo credit to [Airwolfhound](https://www.flickr.com/photos/24874528@N04/3453886876/).
155 |
156 | ---
157 |
158 | [](https://beerpay.io/erikras/ducks-modular-redux) [](https://beerpay.io/erikras/ducks-modular-redux?focus=wish)
159 |
--------------------------------------------------------------------------------
/battlecry/generators/duck/duck.generator.js:
--------------------------------------------------------------------------------
1 | import { Generator, File, namedCasex, casex, log } from 'battlecry';
2 |
3 | const CONFIG_FILE = 'configureStore.js';
4 | const REDUX_PATH = 'src/redux';
5 |
6 | export default class DuckGenerator extends Generator {
7 | config = {
8 | init: {
9 | description: 'Create configStore.js file and an example duck'
10 | },
11 | generate: {
12 | args: 'name ...actions?',
13 | description: 'Create or modify duck to add actions'
14 | }
15 | };
16 |
17 | get configFile() {
18 | const template = this.template(CONFIG_FILE);
19 | const path = `${REDUX_PATH}/${template.filename}`;
20 |
21 | return new File(path);
22 | }
23 |
24 | get actions() {
25 | return (this.args.actions || ['set']).reverse();
26 | }
27 |
28 | init() {
29 | const configFile = this.configFile;
30 | if(configFile.exists) return log.warn(`Modular ducks have already been initiated. Please check the ${configFile.path} file`);
31 |
32 | this.template(CONFIG_FILE).saveAs(configFile.path);
33 | this.generator('duck').setArgs({name: 'todo'}).play('generate');
34 | }
35 |
36 | generate() {
37 | this.addActionsToDuck();
38 | this.addDuckToConfig();
39 | }
40 |
41 | addActionsToDuck() {
42 | const template = this.template('_*');
43 | const path = `${REDUX_PATH}/modules/${template.filename}`;
44 |
45 | let file = new File(path, this.args.name);
46 | if(!file.exists) file = template;
47 |
48 | this.actions.forEach(action => {
49 | file.after('// Actions', `const __NA_ME__ = '${casex(this.args.name, 'na-me')}/__NA-ME__';`, action);
50 |
51 | file.after('switch (action.type) {', [
52 | ' case __NA_ME__:',
53 | ' // Perform action',
54 | ' return state;'
55 | ], action);
56 |
57 | file.after('// Action Creators', [
58 | namedCasex('export function __naMe__() {', + `${action}_${this.args.name}`),
59 | ' return { type: __NA_ME__ };',
60 | '}',
61 | ''
62 | ], action);
63 | });
64 |
65 | file.saveAs(path, this.args.name);
66 | }
67 |
68 | addDuckToConfig() {
69 | const file = this.configFile;
70 | if(!file.exists) return null;
71 |
72 | file
73 | .afterLast('import ', "import __naMe__ from './modules/__naMe__'", this.args.name)
74 | .after('combineReducers({', ' __naMe__,', this.args.name)
75 | .save();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/battlecry/generators/duck/templates/__naMe__.js:
--------------------------------------------------------------------------------
1 | // Actions
2 |
3 | // Reducer
4 | export default function reducer(state = {}, action = {}) {
5 | switch (action.type) {
6 | default: return state;
7 | }
8 | }
9 |
10 | // Action Creators
--------------------------------------------------------------------------------
/battlecry/generators/duck/templates/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, combineReducers } from 'redux';
2 | import createLogger from 'redux-logger';
3 |
4 | const loggerMiddleware = createLogger(); // initialize logger
5 |
6 | const createStoreWithMiddleware = applyMiddleware(loggerMiddleware)(createStore); // apply logger to redux
7 |
8 | const reducer = combineReducers({
9 | });
10 |
11 | const configureStore = (initialState) => createStoreWithMiddleware(reducer, initialState);
12 | export default configureStore;
--------------------------------------------------------------------------------
/duck.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikras/ducks-modular-redux/60390d09e6c5606d5fd4d5f1b2ed4c02572e9692/duck.jpg
--------------------------------------------------------------------------------
/migrate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikras/ducks-modular-redux/60390d09e6c5606d5fd4d5f1b2ed4c02572e9692/migrate.jpg
--------------------------------------------------------------------------------