├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example ├── ExampleApp.js ├── bootstrap.js ├── index.html ├── notepad │ ├── NotepadActions.js │ ├── NotepadContainer.js │ ├── NotepadController.js │ ├── NotepadReducers.js │ └── components │ │ └── Notepad.js └── todo │ ├── TodoActions.js │ ├── TodoContainer.js │ ├── TodoController.js │ ├── TodoReducers.js │ └── components │ ├── TodoItem.js │ └── TodoList.js ├── package.json ├── src ├── App.js ├── Container.js ├── Controller.js ├── Store.js ├── View.js └── index.js ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-object-assign"] 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | root = true 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | node_modules/ 4 | npm-debug.log 5 | todo.txt 6 | dist 7 | es5 -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | todo.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Hristiyan Nikolov 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 | # react-redux-oop 2 | 3 | OOP implementation of redux and react. 4 | 5 | **Moving to 2.0.x, docs will change soon.** 6 | 7 | ## Motivation 8 | 9 | Functional programming is very popular right now among web and mobile frontend developers. It's a cool way of doing things but in my opinion it's never black and white. It's a good paradigm and developers can learn a hell lot out of it but I personally don't like they apps are being organized. I just like many developers don't like plain functions in hundreds lowercase files. I like the class structure, because it eases reading and understanding the code a lot. I believe this implementation of redux and react can be helpful for bigger applications. 10 | 11 | ## Installation 12 | 13 | `npm install --save react-redux-oop` 14 | 15 | The UMD build is in the dist directory. 16 | 17 | ## Description 18 | 19 | This is just an implementation of react and redux. It uses plain `react`, `redux` and `react-redux`. No functionality is changed or added to these libraries. It's only a way to organize your app with a bunch of helpful classes. Also it solves the problem with multiple stores and binded action creators. 20 | 21 | #### Store 22 | 23 | This is a wrapper of the redux store. It's purpose is to be able to easily enable and disable reducers without too much function composition. Also **splits reducers by actions and store path**, instead of just store path. The store requires the usage of [seamless-immutable](https://github.com/rtfeldman/seamless-immutable). It will automatically force it onto your state objects. It also extends [eventemitter3](https://github.com/primus/eventemitter3). 24 | 25 | ```typescript 26 | class Store extends EventEmitter { 27 | constructor(state: Object, enhancer: function) {} 28 | 29 | addReducer(action: string, path: string, reducer: function) {} 30 | removeReducer(action: string, path: string, reducer: function) {} 31 | addReducers(reducers: Object) {} 32 | removeReducers(reducers: Object) {} 33 | 34 | get state(): Object {} 35 | get reduxStore(): Object {} 36 | } 37 | ``` 38 | 39 | ##### Reducers 40 | 41 | Reducers in the Store are added either as a single reducer or as an object of this format: 42 | 43 | ```javascript 44 | store.addReducers({ 45 | 'ADD_ITEM': { 46 | 'todo': [ 47 | (state = {lastId: 0, items: {}}, action) => { 48 | // Do the job 49 | }, 50 | (state = {lastId: 0, items: {}}, action) => { 51 | // Another reducer for the same action and state. 52 | } 53 | ] 54 | }, 55 | 56 | 'REMOVE_ITEM': { 57 | 'todo.items': [ 58 | (state = {}, action) => { 59 | // Do the job 60 | } 61 | ] 62 | }, 63 | 64 | // or you can use wildcards '*' meaning all actions or the full state: 65 | '*': { 66 | '*': [ 67 | (state = {}, action) => { 68 | // Do the job 69 | } 70 | ] 71 | } 72 | }); 73 | ``` 74 | 75 | #### Controller 76 | 77 | This "abstract" class is used to remove the need of action creators. **It combines dispatching with creating the action**. Every controller is provided with the store's dispatch. It also works on the server, because you can create multiple controllers with different stores. Action creators cannot be binded to the dispatch, because the store changes on the server and they are just functions, but controllers can, because they have context. 78 | 79 | ```typescript 80 | class Controller { 81 | constructor(store: Object|Store) {} 82 | 83 | attachTo(store: Object|Store) {} 84 | 85 | dispatch(type: string, data: Object = {}) {} 86 | getState(): Object {} 87 | 88 | get state(): Object {} 89 | } 90 | ``` 91 | 92 | #### Container 93 | 94 | This is an "abstract" extension to the React.Component class. It uses react-redux to connect to the store but used a different syntax and flow to utilize the dispatch method. Every Container component can set an `actions` object of Controllers which are automatically provided with the store from react-redux'es Provider. **Containers are always treated as pure**. 95 | 96 | ```typescript 97 | class Container extends React.Component { 98 | static connect() {} 99 | constructor(props: Object) {} 100 | dispatch(type: string, data: Object) {} 101 | } 102 | ``` 103 | 104 | #### App 105 | 106 | ```typescript 107 | class App { 108 | constructor(state: Object = {}, middlewares: Array. = [], enhancers: Array. = []) {} 109 | 110 | configure(state: Object = {}) {} 111 | renderTo(node: HTMLElement) {} 112 | renderHtml() {} 113 | renderStaticHtml() {} 114 | 115 | _addMiddleware(middleware, index = null) {} 116 | _addEnhancer(enhancer: function, index: number = null) {} 117 | _createStore(state:Object = {}) {} 118 | _render() {} 119 | 120 | get store(): Store {} 121 | } 122 | ``` 123 | 124 | An "abstract" facade class for your application. It's supposed to be extended with custom functionality. It only has a **bootstrapping function** and **may not be used at all** if you don't like it. 125 | 126 | 127 | ## Example 128 | 129 | There is a todo example in the example directory. It shows the full usage. Here is the basic idea: 130 | 131 | ```javascript 132 | class TodoController extends Controller { 133 | addTodo(text) { 134 | this.dispatch('ADD_TODO', {text}); 135 | } 136 | 137 | removeTodo() { 138 | this.dispatch('ADD_TODO', {text}); 139 | } 140 | 141 | asyncAction() { 142 | this.dispatch('ASYNC_START'); 143 | setTimeout(() => this.dispatch('ASYNC_COMPLETE'), 1000); 144 | } 145 | } 146 | 147 | const TodoReducers = { 148 | 'ADD_TODO': { 149 | 'todo': [ 150 | (state = {lastId: 0, items: {}}, action) => { 151 | let newId = state.lastId + 1; 152 | 153 | return { 154 | lastId: newId, 155 | items: state.items.merge({ 156 | [newId]: {text: action.text, checked: false} 157 | }) 158 | } 159 | } 160 | ] 161 | }, 162 | 163 | 'REMOVE_TODO': { 164 | 'todo.items': [ 165 | (state = {}, action) => state.without(action.id) 166 | ] 167 | } 168 | } 169 | 170 | class TodoContainer extends Container { 171 | // This is mapStateToProps from react-redux 172 | static mapper(state) { 173 | return { 174 | items: state.todo.items 175 | }; 176 | } 177 | 178 | // Actions can be defined this way: 179 | actions = { 180 | todo: new TodoController(), 181 | another: new AnotherController(); 182 | } 183 | 184 | // or this way: 185 | actions = new TodoController(); 186 | 187 | render() { 188 | return ( 189 | this.actions.addTodo(text)} 192 | onRemove={id => this.actions.removeTodo(id)} 193 | /> 194 | ); 195 | } 196 | } 197 | 198 | TodoContainer = TodoContainer.connect(); 199 | 200 | 201 | class ExampleApp extends App { 202 | constructor() { 203 | super({ 204 | todo: { 205 | lastId: 0, 206 | items: {} 207 | } 208 | }); 209 | 210 | if (process.env.NODE_ENV !== 'production') { 211 | this._addMiddleware(require('redux-logger')()); 212 | 213 | let matches = window.location.href.match(/[?&]_debug=([^&]+)\b/); 214 | let session = (matches && matches.length) ? matches[1] : null; 215 | 216 | let devTools = null; 217 | if (window['devToolsExtension']) devTools = window['devToolsExtension'](); 218 | 219 | if (devTools) this._addEnhancer(devTools); 220 | } 221 | } 222 | 223 | _render() { 224 | return ( 225 |
226 | 227 |
228 | ); 229 | }; 230 | 231 | _createStore() { 232 | let store = super._createStore(); 233 | store.addReducers(TodoReducers); 234 | return store; 235 | } 236 | } 237 | 238 | 239 | // Create an app and append it to the DOM 240 | 241 | const app = new ExampleApp(); 242 | 243 | app 244 | .configure({/* Change initial state, maybe from server or something. */}) 245 | .renderTo(document.getElementById('app')); 246 | 247 | // We can just create a controller and execute new actions. 248 | 249 | let controller = new TodoController(app.store); 250 | controller.addItem('Have something to eat.'); 251 | controller.addItem('Have something to drink.'); 252 | controller.addItem('Sleep.'); 253 | ``` 254 | -------------------------------------------------------------------------------- /example/ExampleApp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | const App = require('../src/App'); 5 | const TodoContainer = require('./todo/TodoContainer'); 6 | const NotepadContainer = require('./notepad/NotepadContainer'); 7 | 8 | const TodoReducers = require('./todo/TodoReducers'); 9 | const NotepadReducers = require('./notepad/NotepadReducers'); 10 | 11 | 12 | class ExampleApp extends App { 13 | constructor() { 14 | super({ 15 | notepad: { 16 | text: '' 17 | }, 18 | 19 | todo: { 20 | lastId: 0, 21 | items: {} 22 | } 23 | }); 24 | 25 | let dev = (process.env.NODE_ENV !== 'production'); 26 | 27 | if (dev) this._addMiddleware(require('redux-immutable-state-invariant')()); 28 | 29 | this._addMiddleware(require('redux-thunk').default); 30 | 31 | if (dev) { 32 | this._addMiddleware(require('redux-logger')()); 33 | 34 | let matches = window.location.href.match(/[?&]_debug=([^&]+)\b/); 35 | let session = (matches && matches.length) ? matches[1] : null; 36 | 37 | let devTools = null; 38 | if (window['devToolsExtension']) devTools = window['devToolsExtension'](); 39 | 40 | if (devTools) this._addEnhancer(devTools); 41 | if (session) this._addEnhancer(require('redux-devtools').persistState(session)); 42 | } 43 | } 44 | 45 | /** 46 | * @private 47 | */ 48 | _render() { 49 | return ( 50 |
51 | {} 52 | {} 53 |
54 | ); 55 | }; 56 | 57 | /** 58 | * @returns {Store} 59 | * @private 60 | */ 61 | _createStore() { 62 | let store = super._createStore(); 63 | store.addReducers(TodoReducers); 64 | store.addReducers(NotepadReducers); 65 | return store; 66 | } 67 | } 68 | 69 | 70 | module.exports = ExampleApp; 71 | -------------------------------------------------------------------------------- /example/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Create an app and append it to the DOM 4 | 5 | const ExampleApp = require('./ExampleApp'); 6 | const app = new ExampleApp(); 7 | 8 | app.configure().renderTo(document.getElementById('app')); 9 | 10 | 11 | // We can just create a controller and execute new actions. 12 | 13 | const TodoController = require('./todo/TodoController'); 14 | let controller = new TodoController(app.store); 15 | controller.addItem('Have something to eat.'); 16 | controller.addItem('Have something to drink.'); 17 | controller.addItem('Sleep.'); 18 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux + React OOP example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/notepad/NotepadActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const NotepadActions = { 4 | CHANGE_TEXT: 'CHANGE_TEXT' 5 | }; 6 | 7 | module.exports = NotepadActions; -------------------------------------------------------------------------------- /example/notepad/NotepadContainer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | const Container = require('../../src/Container'); 5 | const NotepadController = require('./NotepadController'); 6 | const Notepad = require('./components/Notepad'); 7 | 8 | class NotepadContainer extends Container { 9 | /** 10 | * Map state to props 11 | * @param {Object} state 12 | * @returns {Object} 13 | */ 14 | static mapper(state) { 15 | return { 16 | text: state.notepad.text 17 | }; 18 | } 19 | 20 | /** 21 | * Collection of controllers or a single controller. 22 | * @type {NotepadController|exports|module.exports} 23 | */ 24 | actions = new NotepadController(); 25 | 26 | /** 27 | * Render 28 | * @returns {XML} 29 | */ 30 | render() { 31 | return ( 32 | this.actions.changeText(text)} 35 | /> 36 | ); 37 | } 38 | } 39 | 40 | module.exports = NotepadContainer.connect(); -------------------------------------------------------------------------------- /example/notepad/NotepadController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('../../src/Controller'); 4 | const Actions = require('./NotepadActions'); 5 | 6 | class NotepadController extends Controller { 7 | /** 8 | * @param {string} text 9 | */ 10 | changeText(text) { 11 | this.dispatch(Actions.CHANGE_TEXT, {text}); 12 | } 13 | } 14 | 15 | module.exports = NotepadController; -------------------------------------------------------------------------------- /example/notepad/NotepadReducers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Actions = require('./NotepadActions'); 4 | 5 | const NotepadReducers = { 6 | [Actions.CHANGE_TEXT]: { 7 | 'notepad.text': [ 8 | (state = {}, action) => action.text 9 | ] 10 | } 11 | }; 12 | 13 | module.exports = NotepadReducers; -------------------------------------------------------------------------------- /example/notepad/components/Notepad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const React = require('react'); 4 | 5 | 6 | class Notepad extends React.Component { 7 | static propTypes = { 8 | text: React.PropTypes.string.isRequired, 9 | onChange: React.PropTypes.func.isRequired 10 | }; 11 | 12 | render() { 13 | return ( 14 |
15 |