├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── index.test.js ├── reducerWrapper.test.js └── registry.test.js ├── circle.yml ├── docs ├── _config.yml └── logo.png ├── index.js ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [**.{json,js,ts}] 11 | indent_size = 2 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor-specific 2 | .vscode 3 | .idea 4 | 5 | # coverage 6 | coverage 7 | 8 | # modules 9 | node_modules 10 | bower_components 11 | 12 | # basic tmp 13 | tmp 14 | 15 | # basic build folders 16 | build 17 | build_dist 18 | 19 | # logs 20 | npm-debug.log 21 | npm-debug*.log 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # editor-specific 2 | .vscode 3 | .idea 4 | 5 | # coverage 6 | coverage 7 | 8 | # modules 9 | node_modules 10 | bower_components 11 | 12 | # basic tmp 13 | tmp 14 | 15 | # logs 16 | npm-debug.log 17 | npm-debug*.log 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [1.2.1](https://github.com/asteridux/paradux/compare/v1.2.0...v1.2.1) (2017-02-21) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **publishing:** Publishing actually deployes build folder ([f0e1174](https://github.com/asteridux/paradux/commit/f0e1174)) 12 | 13 | 14 | 15 | 16 | # [1.2.0](https://github.com/asteridux/paradux/compare/v1.1.0...v1.2.0) (2017-01-27) 17 | 18 | 19 | ### Features 20 | 21 | * **all:** Paradux tests and trimming down middleware options ([a5173e3](https://github.com/asteridux/paradux/commit/a5173e3)) 22 | * **compile:** Babel-only compile, removal of webpack ([6c40580](https://github.com/asteridux/paradux/commit/6c40580)) 23 | 24 | 25 | 26 | 27 | # 1.1.0 (2017-01-20) 28 | 29 | 30 | ### Features 31 | 32 | * **all:** Setup entire project ([5171bc2](https://github.com/asteridux/paradux/commit/5171bc2)) 33 | 34 | 35 | 36 | 37 | # 1.0.0 (2017-01-18) 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing To Project 2 | 3 | Please refer to [Standard Version](https://github.com/conventional-changelog/standard-version) for commit reference. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Antonin Januska 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 | ![Logo](docs/logo.png) 2 | 3 | # Paradux 4 | 5 | [![CircleCI](https://circleci.com/gh/asteridux/paradux.svg?style=svg)](https://circleci.com/gh/AntJanus/paradux:Why) 6 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 7 | 8 | ## Why? 9 | 10 | When bootstrapping Redux, Redux assumes that all of your reducers are ready right then and there. There is no way to add new reducers, no way to deregister them, and no way to manipulate interceptors. 11 | 12 | And while the immutable nature of "available Reducers" is great, there are some caveats to this process which Paradux tries to solve. 13 | 14 | ### Self-bootstrapping 15 | 16 | Paradux can create an instance of itself in a module file and while it's being bootstrapped into a Redux store, reducers can register themselves to Paradux like so: 17 | 18 | ```js 19 | // a paradux setup file 20 | import Paradux from 'paradux'; 21 | import defaultReducers from './defaults'; // reducers that are always present 22 | 23 | export default new Paradux(defaultReducers); 24 | ``` 25 | 26 | And additional reducers need only to require that module and register themselves and be immediately available to Redux: 27 | 28 | ```js 29 | // a random reducer file 30 | import paradux from '../paradux'; 31 | 32 | function reducer(state, action) { 33 | // ... all kinds of logic goes here 34 | } 35 | 36 | paradux.register(reducer); // no need to export due to module singleton nature 37 | ``` 38 | 39 | This means that reducers and reducer logic can be kept close to the modules and components it actually affects. Adding new component/modules/reducers doesn't require bloated import statements during the Redux bootstrap process. 40 | 41 | It's a little bit like dependency inversion where the store doesn't "require" all the reducers. The reducers, instead, register themselves. 42 | 43 | ### Code-splitting friendly 44 | 45 | Redux, right now, is not very friendly to code-splitting. Making a reducer available down the line is pretty much unheard of; however, there are plenty of cases for it. 46 | 47 | Since code-splitting requires asynchrony, it can't be used directly within reducers and that's okay. So where does one split? You can't. With Paradux, you can! 48 | 49 | Let's imagine a React component that requires complex reducer logic but only for a subset of certain routes. We can use require ensure for this! 50 | 51 | ```js 52 | // sample route config file 53 | import paradux from './paradux'; 54 | 55 | export default { 56 | component: App, 57 | childRoutes: [ 58 | { 59 | path: '/admin', 60 | getComponent(location, cb) { 61 | require.ensure('./adminReducers', function(require) { 62 | var adminReducers = require('./adminReducers'); 63 | paradux.register(adminReducers); 64 | }) 65 | } 66 | } 67 | ] 68 | } 69 | ``` 70 | 71 | ### Cleanup friendly 72 | 73 | Ever find yourself in a situation where you don't need reducer logic available anymore? Let's consider the previous example and disregard the code splitting portion. Our `adminReducers` are complex, have a ton of logic in them, a ton of switch/case statements. And all of that because they run almost an entirely separate app for a user that is able to login. 74 | 75 | But what if you logged out? Let's consider the following action: 76 | 77 | 78 | ```js 79 | import paradux from './paradox'; 80 | 81 | export function logoutUser() { 82 | return (dispatch) => { 83 | return fetch('/api/logout') 84 | .then((res) => res.toJSON()) 85 | .then(() => { 86 | paradux.deregisterByNamespace('adminReducers'); 87 | 88 | // admin reducers no longer available or run. 89 | dispatch(userLoggedOut()); 90 | }) 91 | ; 92 | } 93 | } 94 | ``` 95 | 96 | The only requirement for deregistration by namespace is to specify the namespace in the first place: 97 | 98 | ```js 99 | paradux.register(adminReducers, 'adminReducers'); 100 | ``` 101 | 102 | ### Middleware - all of the above advantages (WIP) 103 | 104 | The cool thing is that Paradux allows you to add and run middleware whenever you want to as well. You can add logger middleware when you want to, start DevTools when you want to, and so on. On demand and only when needed. 105 | 106 | This opens up a whole new world of possibilities like having toggleable `debug` functionality, logging bootup on certain actions, registering/deregistering middleware on the fly for various situations. 107 | 108 | ## Installation and setup 109 | 110 | Installation: 111 | 112 | ```bash 113 | npm install paradux --save 114 | ``` 115 | 116 | And I suggest creating a separate file for the paradux instance for easy importing: 117 | 118 | ```js 119 | // bootstrap.js file or something similar 120 | import Paradux from 'paradux'; 121 | import defaultReducers from './reducers'; //import any reducers you always want present 122 | 123 | export default const paradux = new Paradux(defaultReducers); 124 | ``` 125 | 126 | You can then import it into your redux store: 127 | 128 | ```js 129 | import { createStore } from 'redux'; 130 | import paradux from './bootstrap'; 131 | 132 | let store = createStore(paradux.reducerWrapper()); 133 | ``` 134 | 135 | ### Adding reducers 136 | 137 | Adding reducers is easy. Given the setup above, you simply need to import the new instance of Paradux and use the `register` function 138 | 139 | ```js 140 | import paradux from './bootstrap'; 141 | 142 | function myReducer(state, action) { 143 | //some logic 144 | } 145 | 146 | paradux.register(myReducer); 147 | ``` 148 | 149 | You can also register a reducer by namespace: 150 | 151 | ```js 152 | paradux.register(myReducer, 'my reducer'); 153 | ``` 154 | 155 | ### Removing reducer 156 | 157 | There are several ways of removing a reducer from the reducer collection in Paradux. 158 | 159 | First, via the returned handler: 160 | 161 | ```js 162 | const removeReducer = paradux.register(myReducer); 163 | 164 | //when you need to remove it 165 | removeReducer(); 166 | ``` 167 | 168 | Second, if you still have the original reducer handy, you can use that to deregister: 169 | 170 | ```js 171 | paradux.redergister(myReducer); 172 | ``` 173 | 174 | Third, if you specified a namespace, you can deregister by namespace: 175 | 176 | ```js 177 | paradux.deregisterByNamespace('my reducer'); 178 | ``` 179 | 180 | ### Non-removable reducers 181 | 182 | There's always a need for reducers that cannot be removed, there are two ways of adding these. 183 | 184 | First, via initialization: 185 | 186 | ```js 187 | var paradux = new Paradux(arrayOfReducersThatCannotBeRemoved); 188 | ``` 189 | 190 | And second, via initializing the reducerWrapper: 191 | 192 | ```js 193 | createStore(paradux.reducerWrapper(arrayOfReducersThatCannotBeRemoved)); 194 | ``` 195 | 196 | You can use a mix and match of either techniques or both at the same time. 197 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import Paradux from '../index' 2 | 3 | describe('Core functionality test', () => { 4 | const paradux = new Paradux([]) 5 | 6 | it('should have an empty collection of reducers', () => { 7 | expect(paradux._reducers.length).toEqual(0) 8 | }) 9 | 10 | it('should register a reducer successfully', () => { 11 | var func = function () { } 12 | 13 | paradux.register(func) 14 | 15 | expect(paradux._reducers.indexOf(func)).toBeGreaterThan(-1) 16 | }) 17 | 18 | it('should register/deregister a reducer successuflly', () => { 19 | var func = () => { } 20 | var deregister = paradux.register(func) 21 | 22 | expect(paradux._reducers.indexOf(func)).toBeGreaterThan(-1) 23 | 24 | deregister() 25 | 26 | expect(paradux._reducers.indexOf(func)).toBeLessThan(0) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /__tests__/reducerWrapper.test.js: -------------------------------------------------------------------------------- 1 | import Paradux from '../index' 2 | 3 | describe('Reducer wrapper test', () => { 4 | it('should return plain state if no reducers registered', () => { 5 | const paradux = new Paradux() 6 | const wrapper = paradux.reducerWrapper() 7 | 8 | var state = { test: 'no val' } 9 | var action = { 10 | type: 'FOO', 11 | payload: { 12 | test: 'val' 13 | } 14 | } 15 | 16 | expect(wrapper(state, action)).toEqual(state) 17 | }) 18 | 19 | it('should modify state based on initial reducer', () => { 20 | const REPLACE = 'REPLACE' 21 | const paradux = new Paradux() 22 | const wrapper = paradux.reducerWrapper([ (state, action) => { 23 | if (action.type === REPLACE) { 24 | return action.payload 25 | } 26 | 27 | return state 28 | }]) 29 | 30 | var state = { test: 'no val' } 31 | var action = { 32 | type: REPLACE, 33 | payload: { 34 | test: 'val' 35 | } 36 | } 37 | 38 | var newState = wrapper(state, action) 39 | 40 | expect(newState).toBeDefined() 41 | expect(newState.test).toEqual('val') 42 | }) 43 | 44 | it('should modify state depending on currently-available reducers', () => { 45 | const REPLACE = 'REPLACE' 46 | const EXTEND = 'EXTEND' 47 | const paradux = new Paradux() 48 | const wrapper = paradux.reducerWrapper() 49 | 50 | const replacer = (state, action) => { 51 | if (action.type === REPLACE) { 52 | return action.payload 53 | } 54 | 55 | return state 56 | } 57 | 58 | const extender = (state, action) => { 59 | if (action.type === EXTEND) { 60 | return Object.assign({}, state, action.payload) 61 | } 62 | 63 | return state 64 | } 65 | 66 | var state = { test: 'no val' } 67 | var replaceAction = { 68 | type: REPLACE, 69 | payload: { 70 | test: 'val' 71 | } 72 | } 73 | 74 | var extendAction = { 75 | type: EXTEND, 76 | payload: { 77 | test2: 'val' 78 | } 79 | } 80 | 81 | paradux.register(replacer) 82 | 83 | var newState = wrapper(state, replaceAction) 84 | 85 | expect(newState).toBeDefined() 86 | expect(newState.test).toEqual('val') 87 | expect(newState.test2).toBeUndefined() 88 | 89 | paradux.deregister(replacer) 90 | paradux.register(extender) 91 | 92 | newState = wrapper(state, extendAction) 93 | 94 | expect(newState).toBeDefined() 95 | expect(newState.test).toEqual('no val') 96 | expect(newState.test2).toEqual('val') 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /__tests__/registry.test.js: -------------------------------------------------------------------------------- 1 | import Paradux from '../index' 2 | 3 | describe('Registry test', () => { 4 | it('should have an empty registry', () => { 5 | const paradux = new Paradux([]) 6 | 7 | expect(Object.keys(paradux._registry).length).toEqual(0) 8 | }) 9 | 10 | it('should register a reducer successfully', () => { 11 | const paradux = new Paradux([]) 12 | 13 | var func = function () { } 14 | 15 | paradux.register(func, 'func') 16 | 17 | expect(paradux._registry.func).toBeDefined() 18 | }) 19 | 20 | it('should register/deregister a reducer successuflly via registry', () => { 21 | const paradux = new Paradux([]) 22 | 23 | var func = () => { } 24 | var deregister = paradux.register(func, 'func') 25 | 26 | expect(paradux._registry.func).toBeDefined() 27 | 28 | paradux.deregisterByNamespace('func') 29 | 30 | expect(paradux._registry.func).toBeUndefined() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.9.1 4 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asteridux/paradux/20b07a9f2eb5131c427f687e86dbeb9071fe8c03/docs/logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export default class Paradux { 2 | 3 | constructor (initialReducers = []) { 4 | this._reducers = initialReducers 5 | this._registry = {} 6 | 7 | this.reducerWrapper = this.reducerWrapper.bind(this) 8 | this.register = this.register.bind(this) 9 | this.deregister = this.deregister.bind(this) 10 | } 11 | 12 | reducerWrapper (initReducers = []) { 13 | this._reducers = this._reducers.concat(initReducers) 14 | 15 | return (state, action) => { 16 | return this._reducers.reduce((collectiveState, reducer) => { 17 | return reducer(collectiveState, action) 18 | }, state) 19 | } 20 | } 21 | 22 | register (reducer, namespace) { 23 | this._reducers.push(reducer) 24 | 25 | var deregister = this.deregister(reducer, namespace) 26 | 27 | if (namespace) { 28 | this._registry[namespace] = { 29 | reducer, 30 | deregister 31 | } 32 | } 33 | 34 | return deregister 35 | } 36 | 37 | deregister (reducer, namespace) { 38 | return () => { 39 | this._reducers.splice(this._reducers.indexOf(reducer), 1) 40 | 41 | if (namespace) { 42 | delete this._registry[namespace] 43 | } 44 | 45 | return true 46 | } 47 | } 48 | 49 | deregisterByNamespace (namespace) { 50 | this._registry[namespace].deregister() 51 | 52 | delete this._registry[namespace] 53 | 54 | return true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paradux", 3 | "version": "1.2.1", 4 | "description": "A Redux middleware that adds uncertainty", 5 | "main": "./build/lib.js", 6 | "scripts": { 7 | "release": "standard-version", 8 | "prepublish": "npm run build", 9 | "prebuild": "rimraf build && mkdir build", 10 | "build": "babel index.js --out-file ./build/lib.js", 11 | "test": "jest", 12 | "test:watch": "jest --watch" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/asteridux/paradux.git" 17 | }, 18 | "author": "Antonin Januska ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/asteridux/paradux/issues" 22 | }, 23 | "homepage": "https://github.com/asteridux/paradux#readme", 24 | "devDependencies": { 25 | "babel-cli": "6.22.2", 26 | "babel-core": "6.21.0", 27 | "babel-jest": "18.0.0", 28 | "babel-preset-env": "1.1.8", 29 | "eslint-config-standard": "6.2.1", 30 | "eslint-plugin-promise": "3.4.0", 31 | "eslint-plugin-standard": "2.0.1", 32 | "jest": "18.1.0", 33 | "standard": "8.6.0" 34 | }, 35 | "jest": { 36 | "bail": true, 37 | "collectCoverage": true, 38 | "verbose": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | cache: true, 6 | entry: './index.js', 7 | output: { 8 | path: path.resolve('./build'), 9 | filename: 'lib.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js?$/, 15 | exclude: /(node_modules)/, 16 | loader: 'babel-loader', 17 | options: { 18 | cacheDirectory: true 19 | } 20 | }, 21 | ] 22 | } 23 | } 24 | --------------------------------------------------------------------------------