|
| 1 | +React Redux Connector |
| 2 | +===================== |
| 3 | + |
| 4 | +Object-oriented React bindings for [Redux](https://github.com/reactjs/redux). |
| 5 | + |
| 6 | +## Installation |
| 7 | + |
| 8 | +``` |
| 9 | +npm install --save react-redux-connector |
| 10 | +``` |
| 11 | + |
| 12 | +## Requirements |
| 13 | + |
| 14 | +Currently, `react-redux-connector` is available only as [npm](http://npmjs.com/) package. |
| 15 | +To effectively work with the library, you will also have to use transpiler like |
| 16 | +[babel](https://babeljs.io/) with [es2015-classes](https://babeljs.io/docs/plugins/transform-es2015-classes) |
| 17 | +and [class-properties](https://babeljs.io/docs/plugins/transform-class-properties) features enabled. |
| 18 | + |
| 19 | +## Motivation |
| 20 | + |
| 21 | +Pretty quickly after starting using `react-redux` I started to feel uncomfortability of having |
| 22 | +my actions, reducer definitions and component code that actually uses former scattered among |
| 23 | +different files and places in the application. Also, I didn't like nor huge switch-case |
| 24 | +reducer functions, nor lots of same-looking small reducer functions each of which |
| 25 | +accepts repetitive (state, action) arguments pair. As a Rails developer, I felt a need of |
| 26 | +some kind of separate abstraction layer that will hoist all the redux-specific logic |
| 27 | +in a declarative and DRY way. That's how the concept of redux `Connector` component was born. |
| 28 | + |
| 29 | +## Documentation |
| 30 | + |
| 31 | +`react-redux-connector` is inspired by Dan Abramov's awesome [react-redux](https://github.com/reactjs/react-redux) |
| 32 | +library. At it's lowest level it uses react-redux's subscription mechanism to |
| 33 | +redux store (this part of code was ported from react-redux), however, `react-redux-connector` |
| 34 | +provides completely different way of organizing react-related logic of your |
| 35 | +application and it's usage. `react-redux-connector` exports `Connector`, `Connection` |
| 36 | +and `Reductor` classes, the most important of which is, unsuprisingly, the `Connector` class. |
| 37 | + |
| 38 | +### Connector |
| 39 | + |
| 40 | +Each Connector hoists all react-related logic (reducer functions, actions, |
| 41 | +dispatching, etc) and provides bindings for your React components (which are |
| 42 | +called connections). |
| 43 | + |
| 44 | +#### API |
| 45 | + |
| 46 | +Connector classes should have following properties set up: |
| 47 | + |
| 48 | +- `static $connection` - the React component (view layer) that Connector provides |
| 49 | +connections for. |
| 50 | + |
| 51 | +- `static $state` - the initial redux state that will be used in reducer function. |
| 52 | + |
| 53 | +- `static $namespace` - the path of connector's `$state` in full redux's state. |
| 54 | +Should be something like `'profile.show'` or `'todos.list'`. It is also used |
| 55 | +for generation of action types (which will look like `'profile.show/$receive'`). |
| 56 | +This property is set automatically in `Connector.reduce` function that generates |
| 57 | +required `$reducer` function (see bellow). But if connector only provides data |
| 58 | +with no action handling, appropriate `$namespace` should be set explicitly. |
| 59 | +Defaults to `'global'`. |
| 60 | + |
| 61 | +- `static $reducer` - a reducer function that should be generated with `Connector.reduce` |
| 62 | +function. **This function should be called on behalf of your connector**, i.e. |
| 63 | +`static $reducer = TodosConnector.reduce(...)`. This function should be called like this: |
| 64 | + |
| 65 | +```js |
| 66 | +static $reducer = YourConnector.reduce('your.namespace', (state) => ({ |
| 67 | + $actionOne: (arg) => newState, |
| 68 | + $otherAction: (arg1, arg2) => anotherNewState |
| 69 | +})); |
| 70 | +``` |
| 71 | + |
| 72 | +By calling `reduce` in this way, your connector's prototype gets `$$actionOne` and |
| 73 | +`$$otherAction` methods that will **dispatch** corresponding action that will |
| 74 | +trigger corresponding reducer's code with arguments you've passed. |
| 75 | + |
| 76 | +- `$expose` instance method that should return object that will be passed |
| 77 | +to $connection component in props. Yes, you can think of it as react-redux's |
| 78 | +`mapStateToProps`. This method accepts 2 arguments: `$state` which is |
| 79 | +current connector's state (i.e. part of the full state under connector's namespace) |
| 80 | +and `state`, which is full redux state. Defaults to function that simply returns `$state`. |
| 81 | + |
| 82 | +#### $-functions |
| 83 | + |
| 84 | +All functions in connector's prototype that start with *exactly one* '$' sign |
| 85 | +will be available in connector's connection component and all other nested |
| 86 | +connection components. |
| 87 | + |
| 88 | +#### Example |
| 89 | + |
| 90 | +```js |
| 91 | +import { Connector } from 'react-redux-connector'; |
| 92 | +import Todos from './Todos'; |
| 93 | +import { get, post, put, destroy } from 'utils'; |
| 94 | + |
| 95 | +export default class TodosConnector extends Connector { |
| 96 | + static $connection = Todos; |
| 97 | + static $state = []; |
| 98 | + static $reducer = TodosConnector.reduce('todos', (state) => ({ |
| 99 | + $receive: (items) => items, |
| 100 | + $addItem: (item) => [...state, item], |
| 101 | + $updateItem: (item) => state.map(i => i.id === item.id ? item : i), |
| 102 | + $removeItem: (id) => state.filter(i => i.id !== id) |
| 103 | + })); |
| 104 | + |
| 105 | + $expose($state) { |
| 106 | + return { items: $state }; |
| 107 | + } |
| 108 | + |
| 109 | + $load() { |
| 110 | + return get('/todos') |
| 111 | + .then(response => this.$$receive(response.data)); |
| 112 | + } |
| 113 | + |
| 114 | + $create(item) { |
| 115 | + return post('/todos', { item }) |
| 116 | + .then(response => this.$$addItem(response.data)); |
| 117 | + } |
| 118 | + |
| 119 | + $update(item) { |
| 120 | + return put(`/todos/${item.id}`, { item }) |
| 121 | + .then(response => this.$$updateItem(response.data)); |
| 122 | + } |
| 123 | + |
| 124 | + $destroy(id) { |
| 125 | + return destroy(`/todos/${id}`) |
| 126 | + .then(() => this.$$removeItem(id)); |
| 127 | + } |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +### Connection |
| 132 | + |
| 133 | +Connection is a very simple helper object that you should inherit from instead of |
| 134 | +`React.Component`. Connection components can call all $-starting methods that |
| 135 | +are defined in Connector (that intended to result in dispatching an action). |
| 136 | + |
| 137 | +Note that only explicitly connected connection (i.e. the one that is specified |
| 138 | +in connector's $connection property) gets connector's exposed state in properties. |
| 139 | +All other connections nested under that 'main' one have usual props that you've |
| 140 | +passed to them via React means, but they **do** have access to connector's $-functions. |
| 141 | + |
| 142 | +#### Example |
| 143 | + |
| 144 | +```js |
| 145 | +import { Connection } from 'react-redux-connector'; |
| 146 | + |
| 147 | +export default class Todos extends Connection { |
| 148 | + state = { title: '' }; |
| 149 | + |
| 150 | + componentDidMount() { |
| 151 | + this.$load(); |
| 152 | + } |
| 153 | + |
| 154 | + saveItem() { |
| 155 | + this.$create({ title: this.state.title }) |
| 156 | + .then(() => this.setState({ title: '' })); |
| 157 | + } |
| 158 | + |
| 159 | + destroyItem(id) { |
| 160 | + this.$destroy(id); |
| 161 | + } |
| 162 | + |
| 163 | + render() { |
| 164 | + return ( |
| 165 | + <div> |
| 166 | + {this.props.items.map(item => |
| 167 | + <div key={item.id}> |
| 168 | + {item.title} |
| 169 | + <button onClick={() => this.destroyItem(item.id)}>Delete</button> |
| 170 | + </div> |
| 171 | + )} |
| 172 | + <input onChange={(e) => this.setState({ title: e.target.value })} /> |
| 173 | + <button onClick={() => this.saveItem()}>Save</button> |
| 174 | + </div> |
| 175 | + ); |
| 176 | + } |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +**NOTE:** if you don't want to inherit from `Connection`, you can gain access to |
| 181 | +connector's $-functions using react context: |
| 182 | + |
| 183 | +```js |
| 184 | +class Todos extends Component { |
| 185 | + static contextTypes = { |
| 186 | + on: PropTypes.object |
| 187 | + }; |
| 188 | + |
| 189 | + componentDidMount() { |
| 190 | + this.context.on.$load(); |
| 191 | + } |
| 192 | + |
| 193 | + // ... |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +Actually, that's only one thing Connection does - provides a kind of syntactic |
| 198 | +sugar for calling connector's $-functions. |
| 199 | + |
| 200 | +### Reductor |
| 201 | + |
| 202 | +Reductor is a special helper component that acts as react-redux's state provider, |
| 203 | +but it's main purpose is to generate a store reducer function for you. On initialization |
| 204 | +it will traverse all children tree looking for connectors, collect them and use |
| 205 | +to create and provide store with given createStore prop. Connectors that are not |
| 206 | +present in the children tree should be listed in `connectors` property. Naturally, |
| 207 | +the most obvious example is usage with [react-router](https://www.npmjs.com/package/react-router) |
| 208 | +routes. |
| 209 | + |
| 210 | +```js |
| 211 | +import { Reductor } from 'react-redux-connector'; |
| 212 | +import { createStore } from 'redux'; |
| 213 | + |
| 214 | +// bellow are Connector components imported |
| 215 | +import Profile from 'application/profile'; |
| 216 | +import { Todos, TodoDetails } from 'application/todos'; |
| 217 | + |
| 218 | +export default class Routes extends Component { |
| 219 | + return ( |
| 220 | + <Reductor createStore={createStore}> |
| 221 | + <Router history={history}> |
| 222 | + <IndexRedirect to="/profile" /> |
| 223 | + <Route path="/profile" component={Profile} /> |
| 224 | + <Route path="/todos" component={Todos} /> |
| 225 | + <Route path="/todos/:id" component={Todos} /> |
| 226 | + </Router> |
| 227 | + </Reductor> |
| 228 | + ); |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +#### Reductor Props |
| 233 | + |
| 234 | +| Prop Name | Spec | Description | |
| 235 | +|-----------------|----------------------------------------------------------|-------------| |
| 236 | +| `createStore` | **required** `PropTypes.func` | A function that takes reducer function (generated by a Reductor) as an argument and returns a redux state | |
| 237 | +| `connectors` | *optional* `PropTypes.arrayOf(PropTypes.func)` | An array of connectors that cannot be mentioned in children tree, but whose `$reducer` functions should become a part of generated redux reducer function | |
| 238 | +| `connectorProp` | *optional* `PropTypes.string`, defaults to `'component'` | A prop of components in children tree that contains a Connector as a value | |
| 239 | + |
| 240 | + |
| 241 | +## License |
| 242 | + |
| 243 | +MIT |
0 commit comments