├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json └── src ├── createInjectableStore.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "extends": "airbnb", 5 | "env": { 6 | "node": true 7 | }, 8 | "plugins": [ 9 | "prefer-object-spread" 10 | ], 11 | "ecmaFeatures": { 12 | "arrowFunctions": true, 13 | "blockBindings": true, 14 | "classes": true, 15 | "defaultParams": true, 16 | "destructuring": true, 17 | "forOf": true, 18 | "generators": false, 19 | "modules": true, 20 | "experimentalObjectRestSpread": true, 21 | "objectLiteralComputedProperties": true, 22 | "objectLiteralDuplicateProperties": false, 23 | "objectLiteralShorthandMethods": true, 24 | "objectLiteralShorthandProperties": true, 25 | "restParams": true, 26 | "spread": true, 27 | "superInFunctions": true, 28 | "templateStrings": true 29 | }, 30 | "rules": { 31 | "new-cap": 0, 32 | "no-use-before-define": 0, 33 | "arrow-body-style": 0, 34 | // triple equals is required except for when comparing with null 35 | "eqeqeq": [2, "allow-null"], 36 | // Allow console methods, which get removed during build process 37 | "no-console": 0, 38 | // Disable `arrow-parens` because it conflicts with Flow. 39 | "arrow-parens": 0, 40 | "prefer-object-spread/prefer-object-spread": 2 41 | }, 42 | "globals": { 43 | "__DEV__": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | lib/ 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | 6 | script: npm test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.1.0 4 | 5 | - Add warning when an attempt is made to inject a reducer into an existing namespace. 6 | - Add `force` argument to `inject` and `injectAll` to allow forcing the injection of a reducer into an existing namespace. 7 | 8 | ## v1.0.0 9 | 10 | Initial release 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Leland Richardson 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-injectable-store 2 | 3 | Redux store with injectable reducers for use with bundle splitting, large apps, and SPAs. 4 | 5 | ## Motivation 6 | 7 | It is often desirable to have a single "global" redux store in your application so it is easy for 8 | reducers to listen and react to actions issued by disparate parts of the application. For sufficiently 9 | large applications though, this becomes problematic because you then need to require all of the reducers 10 | in your entire app at the time of creating your store. This gets more complicated once you start 11 | bundle splitting, or lazily loading more JavaScript in the context of a SPA. 12 | 13 | This library allows reducers to "inject" themselves into the store at any time, solving the above 14 | problems. 15 | 16 | ## Installation 17 | 18 | ```js 19 | npm i --save redux-injectable-store 20 | ``` 21 | 22 | ## API 23 | 24 | Right now, this module exports a single method: `createInjectableStore`. You can use in a CommonJS 25 | environment: 26 | 27 | ES6: 28 | ```js 29 | import { createInjectableStore } from 'redux-injectable-store'; 30 | ``` 31 | 32 | CommonJS: 33 | ```js 34 | const createInjectableStore = require('redux-injectable-store').createInjectableStore; 35 | ``` 36 | 37 | `createInjectableStore` has the following API (extending [Redux's flow types](https://github.com/reactjs/redux/blob/master/flow-typed/redux.js)) 38 | 39 | ```js 40 | 41 | // This is your typical redux store (returned by `createStore(...)` 42 | type ReduxStore = { 43 | dispatch: Dispatch; 44 | getState(): S; 45 | subscribe(listener: () => void): () => void; 46 | replaceReducer(nextReducer: Reducer): void 47 | }; 48 | 49 | // This is a new "injectable" store with some extra methods 50 | type InjectableStore = ReduxStore & { 51 | inject(namespace: string, reducer: Reducer, force: boolean = false), 52 | injectAll({ [key: string]: Reducer }, force: boolean = false), 53 | clearReducers(), 54 | }; 55 | 56 | function createInjectableStore( 57 | preloadedState: S, 58 | enhancer: StoreEnhancer, 59 | wrapReducer: (Reducer): Reducer = Identity 60 | ): InjectableStore; 61 | ``` 62 | 63 | The `createInjectableStore` API follows the same API as Redux's `createStore`, but with the first 64 | argument (`reducer`) missing, and an optional additional last argument, `wrapReducer`. The 65 | `wrapReducer` argument is a function that takes a reducer and returns a reducer. It is meant to allow 66 | you to provide some global action handling if you need to. 67 | 68 | 69 | ## Usage 70 | 71 | ```js 72 | // store.js 73 | 74 | import { createInjectableStore } from 'redux-injectable-store'; 75 | import { applyMiddleware, compose } from 'redux'; 76 | import thunk from 'redux-thunk'; 77 | import pack from 'redux-pack'; 78 | 79 | const initialState = getInitialStoreState(); // can also just be `{}` 80 | 81 | // add whatever middleware you normally would (this is an example) 82 | const enhancer = applyMiddleware( 83 | thunk, 84 | pack.middleware 85 | ); 86 | 87 | const store = createInjectableStore({ 88 | initialState, 89 | enhancer, 90 | }); 91 | 92 | export default store; 93 | ``` 94 | 95 | ```js 96 | // SomeOtherFileA.js 97 | 98 | import store from '../path/to/store'; 99 | import FooReducer from '../path/to/FooReducer'; 100 | 101 | store.inject('foo', FooReducer); 102 | ``` 103 | 104 | ```js 105 | // SomeOtherFileB.js 106 | 107 | import store from '../path/to/store'; 108 | import BarReducer from '../path/to/BarReducer'; 109 | import QooReducer from '../path/to/QooReducer'; 110 | 111 | store.injectAll({ 112 | bar: BarReducer, 113 | qoo: QooReducer, 114 | }); 115 | ``` 116 | 117 | ## Additional Notes 118 | 119 | 1. If you try to inject a reducer into a namespace that already has a reducer without passing the 'force' argument, it will `console.warn` and be a "no op". 120 | 121 | 122 | ## License 123 | 124 | [MIT](LICENSE) 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-injectable-store", 3 | "version": "1.1.0", 4 | "description": "Redux store with injectable reducers for use with bundle splitting, large apps, and SPAs.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "test": "npm run lint", 9 | "lint": "eslint ./", 10 | "build": "babel src --out-dir lib" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/lelandrichardson/redux-injectable-store.git" 15 | }, 16 | "keywords": [ 17 | "redex", 18 | "reducer", 19 | "react", 20 | "mobx" 21 | ], 22 | "author": "Leland Richardson ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/lelandrichardson/redux-injectable-store/issues" 26 | }, 27 | "homepage": "https://github.com/lelandrichardson/redux-injectable-store#readme", 28 | "peerDependencies": { 29 | "redux": "*" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.18.0", 33 | "babel-core": "^6.20.0", 34 | "babel-eslint": "^6.1.0", 35 | "babel-preset-react-native": "^1.9.0", 36 | "eslint": "^3.11.1", 37 | "eslint-config-airbnb": "^12.0.0", 38 | "eslint-plugin-import": "^1.16.0", 39 | "eslint-plugin-jsx-a11y": "^2.2.3", 40 | "eslint-plugin-prefer-object-spread": "^1.0.0", 41 | "eslint-plugin-react": "^6.8.0", 42 | "redux": "^3.6.0" 43 | }, 44 | "dependencies": { 45 | "warning": "^3.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/createInjectableStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import warning from 'warning'; 3 | 4 | const FAKE_INITIAL_REDUCER_NAMESPACE = '___'; 5 | const IDENTITY_REDUCER = (state = null) => state; 6 | 7 | const defaultWrapReducer = reducer => reducer; 8 | 9 | const makeEmptyReducerMap = () => ({ 10 | // putting this here because `combineReducers` will complain if there isn't at least 11 | // one reducer initially. 12 | [FAKE_INITIAL_REDUCER_NAMESPACE]: IDENTITY_REDUCER, 13 | }); 14 | 15 | const createInjectableStore = (preloadedState, enhancer, wrapReducer = defaultWrapReducer) => { 16 | let reducers = makeEmptyReducerMap(); 17 | const store = createStore(wrapReducer(combineReducers(reducers)), preloadedState, enhancer); 18 | 19 | const replace = () => { 20 | store.replaceReducer(wrapReducer(combineReducers(reducers))); 21 | }; 22 | 23 | const clearReducers = () => { 24 | reducers = makeEmptyReducerMap(); 25 | }; 26 | 27 | const inject = (namespace, reducer, force = false) => { 28 | if (reducers[namespace] != null && !force) { 29 | warning( 30 | false, 31 | `Attempted to inject a new reducer in an already existing namespace ('${namespace}') without \`force\`. Skipping.` 32 | ); 33 | return; 34 | } 35 | reducers[namespace] = reducer; 36 | replace(); 37 | }; 38 | 39 | const injectAll = (reducerMap, force = false) => { 40 | let hasChanged = false; 41 | Object.keys(reducerMap).forEach(namespace => { 42 | if (reducers[namespace] != null && !force) { 43 | warning( 44 | false, 45 | 'Attempted to inject a new reducer in an already existing namespace without `force`. Skipping.' 46 | ); 47 | return; 48 | } 49 | reducers[namespace] = reducerMap[namespace]; 50 | hasChanged = true; 51 | }); 52 | if (hasChanged) { 53 | replace(); 54 | } 55 | }; 56 | 57 | return { 58 | ...store, 59 | inject, 60 | injectAll, 61 | clearReducers, 62 | }; 63 | }; 64 | 65 | module.exports = createInjectableStore; 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createInjectableStore from './createInjectableStore'; 2 | 3 | module.exports = { 4 | createInjectableStore, 5 | }; 6 | --------------------------------------------------------------------------------