├── .babelrc
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── doghousediagram.png
├── examples
└── counters
│ ├── public
│ └── index.html
│ ├── server.js
│ ├── src
│ ├── App.css
│ ├── App.js
│ ├── Counter.js
│ ├── index.css
│ └── index.js
│ └── webpack.config.babel.js
├── package.json
├── src
├── ScopedActionFactory.js
├── bindActionCreatorsDeep.js
├── bindScopedActionFactories.js
├── index.js
├── scopeActionCreators.js
├── scopeReducers.js
└── utils
│ ├── memoize.js
│ └── object-shim.js
├── test
├── .eslintrc
├── ScopedActionFactory.spec.js
├── bindActionCreatorsDeep.spec.js
├── bindScopedActionFactories.spec.js
├── helpers
│ ├── actions.js
│ └── reducers.js
├── scopeActionCreators.spec.js
└── scopeReducers.spec.js
└── webpack.config.babel.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es": {
4 | "presets": ["stage-3"]
5 | },
6 | "umd": {
7 | "presets": ["es2015", "stage-3"]
8 | },
9 | "commonjs": {
10 | "presets": ["es2015", "stage-3"]
11 | },
12 | "examples": {
13 | "presets": ["es2015", "stage-3", "react"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "plugins": [
9 | "react"
10 | ],
11 |
12 | "extends": "eslint:recommended",
13 | "rules": {
14 | "camelcase": [1, { "properties": "never" }],
15 | "new-cap": 0,
16 | "no-console": 0,
17 | "no-eval": 2,
18 | "no-multi-spaces": 1,
19 | "no-bitwise": 2,
20 | "no-redeclare": 1,
21 | "no-plusplus": 0,
22 | "no-new": 2,
23 | "no-undef": 2,
24 | "no-underscore-dangle": 0,
25 | "no-unused-vars": 1,
26 | "no-use-before-define": 0,
27 | "one-var": [1, "never"],
28 | "strict": 0,
29 | "no-unsafe-negation": "error",
30 | "no-octal": "error",
31 | "no-octal-escape": "error",
32 | "array-bracket-spacing": [
33 | "error",
34 | "never"
35 | ],
36 | "block-spacing": [
37 | "error",
38 | "always"
39 | ],
40 | "comma-spacing": [
41 | "error",
42 | {
43 | "before": false,
44 | "after": true
45 | }
46 | ],
47 | "computed-property-spacing": [
48 | "error",
49 | "never"
50 | ],
51 | "eol-last": "error",
52 | "indent": [
53 | 2,
54 | 4,
55 | {
56 | "SwitchCase": 1
57 | }
58 | ],
59 | "key-spacing": [
60 | "error",
61 | {
62 | "beforeColon": false,
63 | "afterColon": true
64 | }
65 | ],
66 | "keyword-spacing": [
67 | "error",
68 | {
69 | "before": true,
70 | "after": true,
71 | "overrides": {
72 | "return": {
73 | "after": true
74 | },
75 | "throw": {
76 | "after": true
77 | },
78 | "case": {
79 | "after": true
80 | }
81 | }
82 | }
83 | ],
84 | "linebreak-style": [
85 | "error",
86 | "unix"
87 | ],
88 | "no-multiple-empty-lines": [
89 | "error",
90 | {
91 | "max": 2,
92 | "maxEOF": 1
93 | }
94 | ],
95 | "no-trailing-spaces": "error",
96 | "no-whitespace-before-property": "error",
97 | "object-curly-spacing": [
98 | "error",
99 | "always"
100 | ],
101 | "padded-blocks": [
102 | "error",
103 | "never"
104 | ],
105 | "quotes": [
106 | "error",
107 | "single",
108 | {
109 | "avoidEscape": true
110 | }
111 | ],
112 | "semi-spacing": [
113 | "error",
114 | {
115 | "before": false,
116 | "after": true
117 | }
118 | ],
119 | "semi": [
120 | "error",
121 | "always"
122 | ],
123 | "space-before-blocks": "error",
124 | "space-before-function-paren": [
125 | "error",
126 | {
127 | "anonymous": "always",
128 | "named": "never"
129 | }
130 | ],
131 | "space-in-parens": [
132 | "error",
133 | "never"
134 | ],
135 | "space-infix-ops": "error",
136 | "space-unary-ops": [
137 | "error",
138 | {
139 | "words": true,
140 | "nonwords": false,
141 | "overrides": {}
142 | }
143 | ],
144 | "spaced-comment": [
145 | "error",
146 | "always",
147 | {
148 | "exceptions": [
149 | "-",
150 | "+"
151 | ],
152 | "markers": [
153 | "=",
154 | "!"
155 | ]
156 | }
157 | ],
158 | "unicode-bom": [
159 | "error",
160 | "never"
161 | ],
162 | "arrow-spacing": [
163 | "error",
164 | {
165 | "before": true,
166 | "after": true
167 | }
168 | ],
169 | "no-useless-rename": [
170 | "error",
171 | {
172 | "ignoreDestructuring": false,
173 | "ignoreImport": false,
174 | "ignoreExport": false
175 | }
176 | ],
177 | "no-var": "error",
178 | "object-shorthand": [1, "always"],
179 | "rest-spread-spacing": [
180 | "error",
181 | "never"
182 | ],
183 | "template-curly-spacing": "error",
184 | "yield-star-spacing": [
185 | "error",
186 | "after"
187 | ],
188 | "react/jsx-no-duplicate-props": 2,
189 | "react/jsx-uses-react": 2,
190 | "react/jsx-uses-vars": 1,
191 | "react/react-in-jsx-scope": 2,
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.DS_Store
3 | node_modules
4 | dist
5 | lib
6 | es
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Datadog, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-doghouse
2 |
3 | `redux-doghouse` is a library that aims to make **reusable components** easier to build with Redux by **scoping actions and reducers** to a particular instance of a component.
4 |
5 | 
6 |
7 | It includes tools to help you build **Scoped Actions** and **Scoped Reducers** with minimal modifications to your code. That way, if you build a Redux store for a `Parent` with an **arbitrary number** of `Children` (meaning there can be none, one, or a million of them), actions affecting `Child A` won't affect `Child B` through `Child Z`.
8 |
9 | **You can read more about why we built `redux-doghouse`, and our real-world use-case, in this [blog post](http://engineering.datadoghq.com/redux-doghouse--creating-reusable-react-redux-components-through-scoping/).**
10 |
11 | # Getting Started
12 | ## Installation
13 | ```
14 | npm install redux-doghouse
15 | ```
16 |
17 | ## Running Examples
18 | ### Counters ([Live Demo](https://redux-doghouse-example.now.sh/))
19 | An app that renders an arbitrary number of ``s, with the ability to change the value of one of them at a time, all of them at once, or only the ones with even or odd numbers
20 | ```
21 | npm run counters
22 | ```
23 |
24 | # API
25 | 1. [scopeActionCreators](#scopeactioncreatorsactioncreators-scopeID)
26 | 2. [ScopedActionFactory](#scopedactionfactoryactioncreators)
27 | 3. [bindScopedActionFactories](#bindscopedactionfactoriesactionfactories-dispatch-bindfn)
28 | 4. [bindActionCreatorsDeep](#bindactioncreatorsdeepactioncreatortree-dispatch)
29 | 5. [scopeReducers](#scopereducersreducers)
30 |
31 | ## For Actions
32 | ### `scopeActionCreators(actionCreators, scopeID)`
33 | Adds a scope to the output of a set of action creators
34 |
35 | The `actionCreators` should be in the same format that you would pass to `bindActionCreators`. e.g:
36 | ```javascript
37 | (value) => ({type: SET_FOO, value})
38 | ```
39 | or
40 | ```javascript
41 | {
42 | foo: (value) => ({type: SET_FOO, value})
43 | }
44 | ```
45 | It will then **scope** each of these action creators, so that the resulting action will include the `scopeID`.
46 | ```javascript
47 | {
48 | foo: (value) => ({
49 | type: SET_FOO, value,
50 | scopeID: "[the specified scopeID]"
51 | })
52 | }
53 | ```
54 |
55 | #### Arguments
56 | - `actionCreators` *(Function or Object)*: An action creator, or an object whose values are action creators. The values can also be nested objects whose values are action creators (or more nested objects, and so on).
57 | - `scopeID` *(String or Number)*: An identifier to include in any actions created by the `actionCreators`
58 |
59 | #### Returns
60 |
61 | *(Object or Function)*: An object mimicking the original object, but with each function adding `{ scopeID }` to the *Object* that they return. If you passed a function as `actionCreators`, the return value will also be a single function.
62 |
63 | #### Example
64 | ```javascript
65 | import { scopeActionCreators } from 'redux-doghouse';
66 | import { actionCreators } from './my-actions';
67 |
68 | // Before scoping:
69 | actionCreators.foo('bar');
70 | // Will return
71 | {
72 | type: 'SET_FOO',
73 | value: 'bar'
74 | };
75 | /// After scoping:
76 | scopeActionCreators(actionCreators, 'a').foo('bar')
77 | // Will return
78 | {
79 | type: 'SET_FOO',
80 | value: 'bar',
81 | scopeID: 'a'
82 | }
83 | ```
84 |
85 | ### `ScopedActionFactory(actionCreators)`
86 | Works similarly to `scopeActionCreators`, but with the added benefit of `instanceof` checking. This allows you to write a check to see whether or not a set of action creators is an `instanceof ScopedActionFactory`.
87 |
88 | For example, the included `bindActionCreatorsDeep` function will intelligently bind an object tree of both scoped and un-scoped action creators, depending on whether it's passed plain objects or `ScopedActionFactory` instances.
89 |
90 | #### Arguments
91 | - `actionCreators` *(Function or Object)*: An action creator, or an object whose values are action creators
92 |
93 | #### Returns
94 |
95 | *(ScopedActionFactory)* A class of object with the following:
96 |
97 | ##### Instance Methods
98 | ###### `scope(id): Object`
99 | Runs `scopeActionCreators(id)` on the `actionCreators` that were passed to the `new ScopeActionFactory`, and returns the result.
100 |
101 | #### Example
102 | ```javascript
103 | import { ScopedActionFactory } from 'redux-doghouse';
104 | import { actionCreators } from './my-actions';
105 |
106 | const scopeableActions = new ScopedActionFactory(actionCreators);
107 | const actionCreatorsScopedToA = scopeableActions.scope('a');
108 | const actionCreatorsScopedToB = scopeableActions.scope('b');
109 |
110 | actionCreatorsScopedToA.foo('bar')
111 | // Will return
112 | {
113 | type: SET_FOO,
114 | value: 'bar',
115 | scopeID: 'a'
116 | }
117 | ```
118 |
119 | ### `bindScopedActionFactories(actionFactories, dispatch, [bindFn])`
120 | Takes an object of `actionFactories` and binds them all to a `dispatch` function. By default, it will use Redux's included `bindActionCreators` to do this, but you can specify a `bindFn` to use instead.
121 |
122 | #### Arguments
123 | - `actionFactories` *(Object or ScopedActionFactory)*: A single ScopedActionFactory, or an object whose values are ScopedActionFactories.
124 | - `dispatch` *(Function)*: A function to which the resulting action creators from `actionFactories` should be dispatched; usually this is the `dispatch` method of a Redux store
125 | - [`bindFn`] *(Function)*: If specified, this function will be used to bind resulting action creators to the `dispatch`. If unspecified, Redux's native `bindActionCreators` will be used by default.
126 |
127 | #### Returns
128 | *(Object or ScopedActionFactory)*: An object mimicking the original object, but with each `ScopedActionFactory` generating functions that will immediately dispatch the action returned by the corresponding action creator. If you passed a single factory as `actionFactories`, the return value will also be a single factory.
129 |
130 | #### Example
131 | ```javascript
132 | import { createStore } from 'redux';
133 | import { ScopedActionFactory, bindScopedActionFactories } from 'redux-doghouse';
134 | import { actionCreators, reducers } from './my-redux-component';
135 |
136 | const store = createStore(reducers);
137 | const scopeableActions = {
138 | myComponentActions: new ScopedActionFactory(actionCreators);
139 | }
140 | const boundScopeableActions = bindScopedActionFactories(scopeableActions, store.dispatch);
141 | ```
142 |
143 | ### `bindActionCreatorsDeep(actionCreatorTree, dispatch)`
144 | Extends Redux's native `bindActionCreators` to allow you to bind a whole tree of nested action creator `function`s and `ScopedActionFactory` instances to a `dispatch` function.
145 |
146 | #### Arguments
147 | - `actionCreatorTree` *(Object)*: An object whose values can be:
148 | - A *Function* to be bound to the `dispatch`
149 | - A `ScopedActionFactory` to be bound to the `dispatch`
150 | - Another *Object* containing either of these (or more nested *Object*)
151 | - `dispatch` *(Function)*: A function to which the members of `actionCreatorTree` should be dispatched; usually this is the `dispatch` method of a Redux store
152 |
153 | #### Example
154 | ```javascript
155 | import { bindActionCreatorsDeep, ScopedActionFactory } from 'redux-doghouse';
156 | import { createStore } from 'redux';
157 | import { reducers } from './my-reducers';
158 |
159 | const store = createStore(reducers);
160 |
161 | const actionCreators = {
162 | fooActions: {
163 | bar: (value) => ({type: 'BAR', value})
164 | },
165 | barActions: {
166 | baz: new ScopedActionFactory({
167 | quux: (value) => ({type: 'QUUX'})
168 | })
169 | }
170 | }
171 | const boundActionCreators = bindActionCreatorsDeep(boundActionCreators, store.dispatch);
172 | ```
173 |
174 | ## For Reducers
175 |
176 | ### `scopeReducers(reducers)`
177 | This acts as an extension of Redux's `combineReducers`, which takes an object of `reducers` in the form of `{ [prop]: reducer(state, action) }` pairs and combines them into a single reducer that returns `{ [prop]: state }` pairs. `scopeReducers` goes a step further and returns an object of `{ [scopeID]: { [prop]: state} }` pairs.
178 |
179 | In other words, it will create a reference to your `reducers` for each new `scopeID` you add to your data model, and route scoped actions to their corresponding `scopeID` when reducing a new state.
180 |
181 | #### Arguments
182 | - `reducers` *(Object)*: An object whose keys correspond to property names, and whose values correspond to different reducing functions that need to be combined into one and reused across multiple scopes.
183 |
184 | #### Returns
185 | *(Function)*: A reducer that takes an object of state objects in the form of `{ [scopeID]: state }` pairs, and an action that includes a `scopeID`. The reducer will return a new object mimicking the original object, but for each key:
186 |
187 | 1. For the matching `scopeID`, invoke the `reducers` to construct a new state object with the same shape as the `reducers`
188 | 2. Leave all the other `scopeID`s' state objects unchanged
189 |
190 | #### Example
191 | ```javascript
192 | // Given these actionCreators...
193 | import { scopeActionCreators } from 'redux-doghouse';
194 | const reducers = {
195 | foo: (state = 0, action) => {
196 | switch (action.type) {
197 | case 'INCREMENT_FOO':
198 | return state + 1;
199 | case 'DECREMENT_FOO':
200 | return state - 1;
201 | default:
202 | return state;
203 | }
204 | }
205 | };
206 | const actionCreatorsA = scopeActionCreators({
207 | incrementFoo: () => ({type: 'INCREMENT_FOO'})
208 | }, 'a');
209 |
210 | // Without scoping
211 | import { combineReducers } from 'redux';
212 | const combinedReducers = combineReducers(reducers);
213 | const state = {foo: 0};
214 | combinedReducers(state, actionCreatorsA.incrementFoo());
215 | // Will return
216 | {foo: 1}
217 |
218 | // With scoping
219 | import { scopeReducers } from 'redux-doghouse';
220 | const scopedReducers = scopeReducers(reducers);
221 | const state = {
222 | a: {foo: 0},
223 | b: {foo: 2}
224 | };
225 | scopedReducers(state, actionCreatorsA.incrementFoo());
226 | // Will return
227 | {
228 | a: {foo: 1},
229 | b: {foo: 2}
230 | }
231 | ```
232 |
--------------------------------------------------------------------------------
/doghousediagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DataDog/redux-doghouse/a9082292ea420cc89f0121876832cb713aad8438/doghousediagram.png
--------------------------------------------------------------------------------
/examples/counters/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Redux-Doghouse - Counters Example
7 |
8 |
9 |
10 |
Redux-Doghouse - Counters
11 |
12 | Each counter is written as if it were a tiny Redux app that can dispatch and reduce for
13 | INCREMENT and DECREMENT
14 | actions.
15 |
16 | Redux-Doghouse prevents + or - button-clicks on one
17 | counter to affect all of the others, but allows the row of buttons below to increment or
18 | decrement multiple counters at a time.
19 |