├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── examples ├── README.md ├── counter │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── components │ │ └── Counter.js │ │ ├── index.js │ │ └── reducers │ │ └── index.js ├── real-world │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── app │ │ ├── App.js │ │ ├── actions.js │ │ ├── components │ │ │ └── Explore.js │ │ ├── containers │ │ │ └── Explore.js │ │ ├── index.js │ │ └── reducer.js │ │ ├── common │ │ ├── components │ │ │ ├── List.js │ │ │ ├── Repo.js │ │ │ └── User.js │ │ ├── middleware │ │ │ └── api.js │ │ └── reducers │ │ │ ├── entities.js │ │ │ └── paginate.js │ │ ├── index.js │ │ ├── repoPage │ │ ├── RepoPage.js │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js │ │ ├── root │ │ ├── Root.dev.js │ │ ├── Root.js │ │ ├── Root.prod.js │ │ ├── containers │ │ │ └── DevTools.js │ │ ├── index.js │ │ ├── reducers │ │ │ ├── configuration.js │ │ │ └── index.js │ │ ├── routes.js │ │ └── store │ │ │ ├── configureStore.dev.js │ │ │ ├── configureStore.js │ │ │ └── configureStore.prod.js │ │ └── userPage │ │ ├── UserPage.js │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js └── todos │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ └── index.html │ └── src │ ├── actions │ ├── index.js │ └── index.spec.js │ ├── components │ ├── App.js │ ├── Footer.js │ ├── Link.js │ ├── Todo.js │ └── TodoList.js │ ├── containers │ ├── AddTodo.js │ ├── FilterLink.js │ └── VisibleTodoList.js │ ├── index.js │ └── reducers │ ├── index.js │ ├── todos.js │ └── visibilityFilter.js ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── react-redux-dynamic-reducer ├── .babelrc ├── .npmignore ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── index.d.ts │ ├── index.js │ └── withReducer.js └── test │ ├── setup.js │ ├── typescript │ ├── definitions-spec.js │ └── definitions │ │ └── withReducer.tsx │ └── withReducer-spec.js └── redux-dynamic-reducer ├── .babelrc ├── .npmignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── createDynamicReducer.js ├── createStore.js ├── filteredReducer.js ├── flattenReducers.js ├── index.d.ts └── index.js └── test ├── createDynamicReducer-spec.js ├── createStore-spec.js ├── filteredReducer-spec.js ├── flattenReducers-spec.js ├── setup.js └── typescript ├── definitions-spec.js └── definitions ├── attachReducers.ts └── createStore.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | .nyc_output/ 4 | coverage/ 5 | webpack.config.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6 10 | }, 11 | "globals": { 12 | "assert": true, 13 | "expect": true, 14 | "sinon": true 15 | }, 16 | "parser": "babel-eslint", 17 | "plugins": [ 18 | "react" 19 | ], 20 | "extends": ["eslint:recommended", "plugin:react/recommended"], 21 | "rules": { 22 | "no-console": ["warn", { "allow": ["assert"] }], 23 | "react/prop-types": ["error", { "skipUndeclared": true }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | lerna-debug.log 4 | .nyc_output 5 | coverage 6 | .vscode 7 | .idea 8 | lib/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | node_modules 3 | npm-debug.log 4 | test/ 5 | docs/ 6 | examples/ 7 | .nyc_output 8 | coverage 9 | .vscode 10 | .idea 11 | .babelrc 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - npm run bootstrap 6 | - npm run test -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to redux-dynamic-reducer 2 | 3 | The following is a set of guidelines for contributing to redux-dynamic-reducer, which are hosted in the [IOOF Holdings Limited Organization](https://github.com/ioof-holdings) on GitHub. 4 | These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a [pull request](#pull-requests). 5 | 6 | #### Table of Contents 7 | 8 | * [How Can I Contribute?](#how-can-i-contribute) 9 | * [Reporting Bugs](#reporting-bugs) 10 | * [Suggesting Enhancements](#suggesting-enhancements) 11 | * [Pull Requests](#pull-requests) 12 | 13 | ## How Can I Contribute? 14 | 15 | ### Reporting Bugs 16 | 17 | Bugs are tracked as [GitHub issues](https://github.com/ioof-holdings/redux-dynamic-reducer/issues). Please check to see if your issue has already been raised before submitting your bug report. 18 | 19 | When submitting a bug report, please include as much information as possible to help contributors to identify the cause of the problem. The ideal bug report would include: 20 | 21 | * **A clear and descriptive title** for the issue to identify the problem. 22 | * **A description of the exact scenario to reproduce the problem** in as much detail as possible. 23 | * **An explanation of the behaviour you expected to see instead and why.** 24 | * **An example to demonstrate the scenario** such as: 25 | * Include copy/pastable snippets (using markdown) in your bug report. 26 | * Links to files in GitHub or other public repository. 27 | * Submit a pull request with [an example](/examples) highlighting the issue. 28 | * **Environment details** such as: 29 | * Browser(s) you have seen the problem in. 30 | * Version of redux-dynamic-reducer you are using. 31 | * Which Redux middleware (including version numbers) are being used. 32 | * What other packages (including version numbers) are being used. 33 | 34 | ### Suggesting Enhancements 35 | 36 | Enhancements are tracked as [GitHub issues](https://github.com/ioof-holdings/redux-dynamic-reducer/issues). Please check to see if your suggestion has already been made before submitting your suggestion. 37 | 38 | When submitting an enhancement submission, please include as much information as possible to help contributors to understand an implement your idea. The ideal enhancement suggestion would include: 39 | 40 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 41 | * **A description of the specifics of your suggestion** in as much detail as possible. 42 | * **An explanation of the benefits implementing your enhancement would provide** 43 | * **An example to demonstrate the scenario** such as: 44 | * Include copy/pastable snippets (using markdown) in your enhancement submission. 45 | * Links to files in GitHub or other public repository. 46 | * Submit a pull request with [an example](/examples) showing how your enhancement would be used. 47 | 48 | ### Pull Requests 49 | 50 | If you want to get your hands dirty, please take a look at the [open issues](https://github.com/ioof-holdings/redux-dynamic-reducer/issues?q=is%3Aissue%20is%3Aopen) and submit a pull request with your proposed solution, [referencing the issue](https://help.github.com/articles/closing-issues-via-commit-messages/) in commit message. 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, IOOF Holdings Limited 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name IOOF nor the names of its contributors may be used to 17 | endorse or promote products derived from this software without specific 18 | prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-dynamic-reducer 2 | 3 | ## Deprecated 4 | 5 | Our new redux library, [redux-dynostore](https://github.com/ioof-holdings/redux-dynostore) has all the features of this one and more, allowing for a lot more dynamic store features. If you experience any difficulty switching over to [redux-dynostore](https://github.com/ioof-holdings/redux-dynostore) then please let us know by raising an issue over there. This library will be subject to major bug fixes and security fixes only. 6 | 7 | [![npm version](https://img.shields.io/npm/v/redux-dynamic-reducer.svg?style=flat-square)](https://www.npmjs.com/package/redux-dynamic-reducer) 8 | [![npm downloads](https://img.shields.io/npm/dm/redux-dynamic-reducer.svg?style=flat-square)](https://www.npmjs.com/package/redux-dynamic-reducer) 9 | [![License: MIT](https://img.shields.io/npm/l/redux-dynamic-reducer.svg?style=flat-square)](/LICENSE.md) 10 | 11 | Use this library to attach additional reducer functions to an existing [Redux](http://redux.js.org/) store at runtime. 12 | 13 | This solution is based on an example proposed by Dan Abramov in a [StackOverflow answer](http://stackoverflow.com/questions/32968016/how-to-dynamically-load-reducers-for-code-splitting-in-a-redux-application#33044701). 14 | 15 | ## Why this library? 16 | 17 | A Redux store's state tree is created from a single reducer function. [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) is the mechanism to compose many reducer functions into a single reducer that can be used to build a hierarchical the state tree. It is not possible to modify the reducer function after the store has been initialised. 18 | 19 | This library allows you to attach new reducer functions after the store is initialised. This is helpful if you want to use a single global store across a lazily loaded application where not all reducers are available at store creation. It also provides a convenience functionality that pairs with [redux-subspace](https://github.com/ioof-holdings/redux-subspace) and allows combining a React component with a reducer that automatically attaches to the store when the component is mounted. 20 | 21 | ## The common use case 22 | 23 | This library will help if you want to lazily load and execute pieces of your application but still manage your state in a global store. You can initialise the store in your first page load and efficiently load a skeleton app while the rest of your app is pulled down and loaded asynchronously. 24 | 25 | This library pairs well with [redux-subspace](https://github.com/ioof-holdings/redux-subspace) for building complex single-page-applications composed of many decoupled micro frontends. 26 | 27 | ## Packages 28 | 29 | * [`redux-dynamic-reducer`](/packages/redux-dynamic-reducer): The core package for `redux-dynamic-reducer` 30 | * [`react-redux-dynamic-reducer`](/packages/react-redux-dynamic-reducer): React bindings for `redux-dynamic-reducer` 31 | 32 | ## How to use 33 | 34 | ### 1. Create the store 35 | 36 | The `createStore` function replaces the [Redux `createStore` function](http://redux.js.org/docs/api/createStore.html). It adds the `attachReducers()` function to the store object. It also supports all the built in optional parameters: 37 | 38 | ```javascript 39 | import { combineReducers } from 'redux' 40 | import { createStore } from 'redux-dynamic-reducer' 41 | 42 | ... 43 | 44 | const reducer = combineReducers({ staticReducer1, staticReducer2 }) 45 | const store = createStore(reducer) 46 | ``` 47 | 48 | ```javascript 49 | const store = createStore(reducer, { initial: 'state' }) 50 | ``` 51 | 52 | ```javascript 53 | const store = createStore(reducer, applyMiddleware(middleware)) 54 | ``` 55 | 56 | ```javascript 57 | const store = createStore(reducer, { initial: 'state' }, applyMiddleware(middleware)) 58 | ``` 59 | 60 | ### 2. Dynamically attach a reducer 61 | 62 | #### Not using redux-subspace 63 | 64 | Call `attachReducers` on the store with your dynamic reducers to attach them to the store at runtime: 65 | 66 | ```javascript 67 | store.attachReducers({ dynamicReducer }) 68 | ``` 69 | 70 | Multiple reducers can be attached as well: 71 | 72 | ```javascript 73 | store.attachReducers({ dynamicReducer1, dynamicReducer2 }) 74 | ``` 75 | 76 | Reducers can also be added to nested locations in the store: 77 | 78 | ```javascript 79 | store.attachReducers({ 80 | some: { 81 | path: { 82 | to: { 83 | dynamicReducer 84 | } 85 | } 86 | } 87 | } ) 88 | ``` 89 | 90 | ```javascript 91 | store.attachReducers({ 'some.path.to': { dynamicReducer } } } }) 92 | ``` 93 | 94 | ```javascript 95 | store.attachReducers({ 'some/path/to': { dynamicReducer } } } }) 96 | ``` 97 | 98 | #### When using React and redux-subspace 99 | 100 | First, wrap the component with `withReducer`: 101 | 102 | ```javascript 103 | // in child component 104 | import { withReducer } from 'react-redux-dynamic-reducer' 105 | 106 | export default withReducer(myReducer, 'defaultKey')(MyComponent) 107 | ``` 108 | 109 | The `withReducer` higher-order component (HOC) bundles a reducer with a React component. `defaultKey` is used by redux-subspace to subspace the default instance of this component. 110 | 111 | Mount your component somewhere inside a react-redux `Provider`: 112 | 113 | ```javascript 114 | // in parent app/component 115 | import MyComponent from './MyComponent' 116 | 117 | 118 | ... 119 | 120 | ... 121 | 122 | ``` 123 | 124 | When the component is mounted, the reducer will be automatically attached to the Provided Redux store. It will also mount the component within a [subspace](https://github.com/ioof-holdings/redux-subspace) using the default key. 125 | 126 | Multiple instances of the same component can be added by overriding the default subspace key with an instance specific key: 127 | 128 | ```javascript 129 | // in parent app/component 130 | import MyComponent from './MyComponent' 131 | 132 | ... 133 | 134 | const MyComponent1 = MyComponent.createInstance('myInstance1') 135 | const MyComponent2 = MyComponent.createInstance('myInstance2') 136 | 137 | ... 138 | 139 | 140 | 141 | 142 | 143 | ``` 144 | 145 | Additional state can be mapped for the component or an instance of the component by providing an additional mapper: 146 | 147 | ```javascript 148 | export default withReducer(myReducer, 'defaultKey', { mapExtraState: (state, rootState) => ({ /* ... */ }) })(MyComponent) 149 | 150 | ... 151 | 152 | const MyComponentInstance = MyComponent 153 | .createInstance('instance') 154 | .withExtraState((state, rootState) => ({ /* ... */ }) }) 155 | 156 | ... 157 | 158 | const MyComponentInstance = MyComponent 159 | .createInstance('instance') 160 | .withOptions({ mapExtraState: (state, rootState) => ({ /* ... */ }) }) 161 | ``` 162 | 163 | The extra state is merged with the bundled reducer's state. 164 | 165 | By default, the components are [namespaced](https://github.com/ioof-holdings/redux-subspace#namespacing). If namespacing is not wanted for a component or and instance of the component, an options object can be provided to prevent it: 166 | 167 | ```javascript 168 | export default withReducer(myReducer, 'defaultKey', { namespaceActions: false })(MyComponent) 169 | 170 | ... 171 | 172 | const MyComponentInstance = MyComponent.createInstance('instance').withOptions({ namespaceActions: false }) 173 | ``` 174 | 175 | ## Examples 176 | 177 | Examples can be found [here](/examples). 178 | 179 | ## Limitations 180 | 181 | * Each dynamic reducer needs a unique key 182 | * If the same key is used in a subsequent attachment, the original reducer will be replaced 183 | * Nested reducers cannot be attached to nodes of the state tree owned by a static reducer 184 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | redux-dynamic-store is distributed with a few examples in its [source code](/examples). Most of these examples are also on [CodeSandbox](https://codesandbox.io/), this is an online editor that lets you play with the examples online. 4 | 5 | ## redux-dynamic-reducer 6 | 7 | * [Counter](/examples/counter) 8 | 9 | ## react-redux-dynamic-reducer 10 | 11 | * [Todos](/examples/todos) 12 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter Example 2 | 3 | This example extends [Redux's counter example](https://github.com/reactjs/redux/tree/master/examples/counter) to attach reducer after store creation. 4 | 5 | To run the example locally: 6 | 7 | ```sh 8 | git clone https://github.com/ioof-holdings/redux-dynamic-reducer.git 9 | 10 | cd redux-dynamic-reducer/examples/counter 11 | npm install 12 | npm start 13 | ``` 14 | 15 | Or check out the [sandbox](https://codesandbox.io/s/github/ioof-holdings/redux-dynamic-reducer/tree/master/examples/counter). 16 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "2.0.2", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.0.17" 7 | }, 8 | "dependencies": { 9 | "prop-types": "^15.6.0", 10 | "react": "^16.0.0", 11 | "react-dom": "^16.0.0", 12 | "redux": "^3.7.2", 13 | "redux-dynamic-reducer": "^2.0.2" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "eject": "react-scripts eject", 19 | "test": "echo 'No Tests'" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Dynamic Reducer Counter Example 7 | 8 | 9 |
10 | 18 | 19 | -------------------------------------------------------------------------------- /examples/counter/src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Counter extends Component { 5 | constructor(props) { 6 | super(props) 7 | this.incrementAsync = this.incrementAsync.bind(this) 8 | this.incrementIfOdd = this.incrementIfOdd.bind(this) 9 | } 10 | 11 | incrementIfOdd() { 12 | if (this.props.value % 2 !== 0) { 13 | this.props.onIncrement() 14 | } 15 | } 16 | 17 | incrementAsync() { 18 | setTimeout(this.props.onIncrement, 1000) 19 | } 20 | 21 | render() { 22 | const { value, onIncrement, onDecrement } = this.props 23 | return ( 24 |

25 | Clicked: {value} times 26 | {' '} 27 | 30 | {' '} 31 | 34 | {' '} 35 | 38 | {' '} 39 | 42 |

43 | ) 44 | } 45 | } 46 | 47 | Counter.propTypes = { 48 | value: PropTypes.number.isRequired, 49 | onIncrement: PropTypes.func.isRequired, 50 | onDecrement: PropTypes.func.isRequired 51 | } 52 | 53 | export default Counter 54 | -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { createStore } from "redux-dynamic-reducer" 4 | 5 | const store = createStore() 6 | const rootEl = document.getElementById("root") 7 | 8 | const render = (Counter) => { 9 | ReactDOM.render( 10 | store.dispatch({ type: "INCREMENT" })} 13 | onDecrement={() => store.dispatch({ type: "DECREMENT" })} 14 | />, 15 | rootEl 16 | ) 17 | } 18 | 19 | const counterPromise = import('./components/Counter') 20 | const reducerModule = import('./reducers') 21 | 22 | Promise.all([reducerModule, counterPromise]).then(modules => { 23 | store.attachReducers({ dynamicCounter: modules[0].default }) 24 | render(modules[1].default) 25 | store.subscribe(() => render(modules[1].default)) 26 | }) 27 | -------------------------------------------------------------------------------- /examples/counter/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default (state = 0, action) => { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1 5 | case 'DECREMENT': 6 | return state - 1 7 | default: 8 | return state 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/real-world/README.md: -------------------------------------------------------------------------------- 1 | # Real World Example 2 | 3 | This example extends [Redux's Real World example](https://github.com/reactjs/redux/tree/master/examples/real-world) to create many of the components as isolated redux component. 4 | 5 | To run the example locally: 6 | 7 | ```sh 8 | git clone https://github.com/ioof-holdings/redux-dynamic-reducer.git 9 | 10 | cd redux-dynamic-reducer/examples/real-world 11 | npm install 12 | npm start 13 | ``` 14 | 15 | Or check out the [sandbox](https://codesandbox.io/s/github/ioof-holdings/redux-dynamic-reducer/tree/master/examples/real-world). 16 | -------------------------------------------------------------------------------- /examples/real-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-world", 3 | "version": "2.0.2", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.0.17", 7 | "redux-devtools": "^3.4.0", 8 | "redux-devtools-dock-monitor": "^1.1.2", 9 | "redux-devtools-log-monitor": "^1.3.0", 10 | "redux-logger": "^3.0.6" 11 | }, 12 | "dependencies": { 13 | "humps": "^2.0.1", 14 | "lodash": "^4.17.4", 15 | "normalizr": "^3.2.4", 16 | "prop-types": "^15.6.0", 17 | "react": "^16.0.0", 18 | "react-dom": "^16.0.0", 19 | "react-loadable": "^5.3.1", 20 | "react-redux": "^5.0.6", 21 | "react-redux-dynamic-reducer": "^2.0.2", 22 | "react-redux-subspace": "^2.0.8", 23 | "react-router": "^3.2.0", 24 | "react-router-redux": "^4.0.8", 25 | "redux": "^3.7.2", 26 | "redux-dynamic-reducer": "^2.0.2", 27 | "redux-subspace": "^2.0.8", 28 | "redux-subspace-wormhole": "^2.0.8", 29 | "redux-thunk": "^2.2.0" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "eject": "react-scripts eject", 35 | "test": "echo 'No Tests'" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/real-world/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Dynamic Reducer Real World Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/real-world/src/app/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | import { connect } from 'react-redux' 6 | import { browserHistory } from 'react-router' 7 | import Explore from './containers/Explore' 8 | import { resetErrorMessage } from './actions' 9 | 10 | class App extends Component { 11 | static propTypes = { 12 | // Injected by React Redux 13 | errorMessage: PropTypes.string, 14 | resetErrorMessage: PropTypes.func.isRequired, 15 | inputValue: PropTypes.string.isRequired, 16 | // Injected by React Router 17 | children: PropTypes.node 18 | } 19 | 20 | handleDismissClick = e => { 21 | this.props.resetErrorMessage() 22 | e.preventDefault() 23 | } 24 | 25 | handleChange = nextValue => { 26 | browserHistory.push(`/${nextValue}`) 27 | } 28 | 29 | renderErrorMessage() { 30 | const { errorMessage } = this.props 31 | if (!errorMessage) { 32 | return null 33 | } 34 | 35 | return ( 36 |

37 | {errorMessage} 38 | {' '} 39 | 42 |

43 | ) 44 | } 45 | 46 | render() { 47 | const { children, inputValue } = this.props 48 | return ( 49 |
50 | 52 |
53 | {this.renderErrorMessage()} 54 | {children} 55 |
56 | ) 57 | } 58 | } 59 | 60 | const mapStateToProps = (state, ownProps) => ({ 61 | errorMessage: state.errorMessage, 62 | inputValue: ownProps.location.pathname.substring(1) 63 | }) 64 | 65 | export default connect(mapStateToProps, { 66 | resetErrorMessage 67 | })(App) 68 | -------------------------------------------------------------------------------- /examples/real-world/src/app/actions.js: -------------------------------------------------------------------------------- 1 | export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE' 2 | 3 | export const resetErrorMessage = () => ({ 4 | type: RESET_ERROR_MESSAGE 5 | }) 6 | -------------------------------------------------------------------------------- /examples/real-world/src/app/components/Explore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | export default class Explore extends Component { 7 | static propTypes = { 8 | value: PropTypes.string.isRequired, 9 | onChange: PropTypes.func.isRequired, 10 | githubRepo: PropTypes.string.isRequired 11 | } 12 | 13 | componentWillReceiveProps(nextProps) { 14 | if (nextProps.value !== this.props.value) { 15 | this.setInputValue(nextProps.value) 16 | } 17 | } 18 | 19 | getInputValue = () => { 20 | return this.input.value 21 | } 22 | 23 | setInputValue = (val) => { 24 | // Generally mutating DOM is a bad idea in React components, 25 | // but doing this for a single uncontrolled field is less fuss 26 | // than making it controlled and maintaining a state for it. 27 | this.input.value = val 28 | } 29 | 30 | handleKeyUp = (e) => { 31 | if (e.keyCode === 13) { 32 | this.handleGoClick() 33 | } 34 | } 35 | 36 | handleGoClick = () => { 37 | this.props.onChange(this.getInputValue()) 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 |

Type a username or repo full name and hit 'Go':

44 | this.input = input} 46 | defaultValue={this.props.value} 47 | onKeyUp={this.handleKeyUp} /> 48 | 51 |

52 | Code on Github. 53 |

54 |

55 | Move the DevTools with Ctrl+W or hide them with Ctrl+H. 56 |

57 |
58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/real-world/src/app/containers/Explore.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Explore from '../components/Explore' 3 | 4 | const mapStateToProps = (state) => ({ 5 | githubRepo: state.configuration.GITHUB_REPO 6 | }) 7 | 8 | export default connect(mapStateToProps)(Explore) 9 | -------------------------------------------------------------------------------- /examples/real-world/src/app/index.js: -------------------------------------------------------------------------------- 1 | import { withReducer } from 'react-redux-dynamic-reducer' 2 | 3 | import App from './App' 4 | import reducer from './reducer' 5 | 6 | export default withReducer(reducer, 'app')(App) 7 | -------------------------------------------------------------------------------- /examples/real-world/src/app/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { RESET_ERROR_MESSAGE } from './actions' 3 | 4 | const errorMessage = (state = null, action) => { 5 | const { type, error } = action 6 | 7 | if (type === RESET_ERROR_MESSAGE) { 8 | return null 9 | } else if (error) { 10 | return error 11 | } 12 | 13 | return state 14 | } 15 | 16 | export default combineReducers({ 17 | errorMessage 18 | }) 19 | -------------------------------------------------------------------------------- /examples/real-world/src/common/components/List.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | export default class List extends Component { 7 | static propTypes = { 8 | loadingLabel: PropTypes.string.isRequired, 9 | pageCount: PropTypes.number, 10 | renderItem: PropTypes.func.isRequired, 11 | items: PropTypes.array.isRequired, 12 | isFetching: PropTypes.bool.isRequired, 13 | onLoadMoreClick: PropTypes.func.isRequired, 14 | nextPageUrl: PropTypes.string 15 | } 16 | 17 | static defaultProps = { 18 | isFetching: true, 19 | loadingLabel: 'Loading...' 20 | } 21 | 22 | renderLoadMore() { 23 | const { isFetching, onLoadMoreClick } = this.props 24 | return ( 25 | 30 | ) 31 | } 32 | 33 | render() { 34 | const { 35 | isFetching, nextPageUrl, pageCount, 36 | items, renderItem, loadingLabel 37 | } = this.props 38 | 39 | const isEmpty = items.length === 0 40 | if (isEmpty && isFetching) { 41 | return

{loadingLabel}

42 | } 43 | 44 | const isLastPage = !nextPageUrl 45 | if (isEmpty && isLastPage) { 46 | return

Nothing here!

47 | } 48 | 49 | return ( 50 |
51 | {items.map(renderItem)} 52 | {pageCount > 0 && !isLastPage && this.renderLoadMore()} 53 |
54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/real-world/src/common/components/Repo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router' 4 | 5 | const Repo = ({ repo, owner }) => { 6 | const { login } = owner 7 | const { name, description } = repo 8 | 9 | return ( 10 |
11 |

12 | 13 | {name} 14 | 15 | {' by '} 16 | 17 | {login} 18 | 19 |

20 | {description && 21 |

{description}

22 | } 23 |
24 | ) 25 | } 26 | 27 | Repo.propTypes = { 28 | repo: PropTypes.shape({ 29 | name: PropTypes.string.isRequired, 30 | description: PropTypes.string 31 | }).isRequired, 32 | owner: PropTypes.shape({ 33 | login: PropTypes.string.isRequired 34 | }).isRequired 35 | } 36 | 37 | export default Repo 38 | -------------------------------------------------------------------------------- /examples/real-world/src/common/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router' 4 | 5 | const User = ({ user }) => { 6 | const { login, avatarUrl, name } = user 7 | 8 | return ( 9 |
10 | 11 | {login} 12 |

13 | {login} {name && ({name})} 14 |

15 | 16 |
17 | ) 18 | } 19 | 20 | User.propTypes = { 21 | user: PropTypes.shape({ 22 | login: PropTypes.string.isRequired, 23 | avatarUrl: PropTypes.string.isRequired, 24 | name: PropTypes.string 25 | }).isRequired 26 | } 27 | 28 | export default User 29 | -------------------------------------------------------------------------------- /examples/real-world/src/common/middleware/api.js: -------------------------------------------------------------------------------- 1 | import { normalize, schema } from 'normalizr' 2 | import { camelizeKeys } from 'humps' 3 | 4 | // Extracts the next page URL from Github API response. 5 | const getNextPageUrl = response => { 6 | const link = response.headers.get('link') 7 | if (!link) { 8 | return null 9 | } 10 | 11 | const nextLink = link.split(',').find(s => s.indexOf('rel="next"') > -1) 12 | if (!nextLink) { 13 | return null 14 | } 15 | 16 | return nextLink.split(';')[0].slice(1, -1) 17 | } 18 | 19 | // Fetches an API response and normalizes the result JSON according to schema. 20 | // This makes every API response have the same shape, regardless of how nested it was. 21 | const callApi = (apiRoot, endpoint, schema) => { 22 | const fullUrl = (endpoint.indexOf(apiRoot) === -1) ? apiRoot + endpoint : endpoint 23 | 24 | return fetch(fullUrl) 25 | .then(response => 26 | response.json().then(json => { 27 | if (!response.ok) { 28 | return Promise.reject(json) 29 | } 30 | 31 | const camelizedJson = camelizeKeys(json) 32 | const nextPageUrl = getNextPageUrl(response) 33 | 34 | return Object.assign({}, 35 | normalize(camelizedJson, schema), 36 | { nextPageUrl } 37 | ) 38 | }) 39 | ) 40 | } 41 | 42 | // We use this Normalizr schemas to transform API responses from a nested form 43 | // to a flat form where repos and users are placed in `entities`, and nested 44 | // JSON objects are replaced with their IDs. This is very convenient for 45 | // consumption by reducers, because we can easily build a normalized tree 46 | // and keep it updated as we fetch more data. 47 | 48 | // Read more about Normalizr: https://github.com/paularmstrong/normalizr 49 | 50 | // GitHub's API may return results with uppercase letters while the query 51 | // doesn't contain any. For example, "someuser" could result in "SomeUser" 52 | // leading to a frozen UI as it wouldn't find "someuser" in the entities. 53 | // That's why we're forcing lower cases down there. 54 | 55 | const userSchema = new schema.Entity('users', {}, { 56 | idAttribute: user => user.login.toLowerCase() 57 | }) 58 | 59 | const repoSchema = new schema.Entity('repos', { 60 | owner: userSchema 61 | }, { 62 | idAttribute: repo => repo.fullName.toLowerCase() 63 | }) 64 | 65 | // Schemas for Github API responses. 66 | export const Schemas = { 67 | USER: userSchema, 68 | USER_ARRAY: [userSchema], 69 | REPO: repoSchema, 70 | REPO_ARRAY: [repoSchema] 71 | } 72 | 73 | // Action key that carries API call info interpreted by this Redux middleware. 74 | export const CALL_API = 'Call API' 75 | 76 | // A Redux middleware that interprets actions with CALL_API info specified. 77 | // Performs the call and promises when such actions are dispatched. 78 | export default store => next => action => { 79 | const callAPI = action[CALL_API] 80 | if (typeof callAPI === 'undefined') { 81 | return next(action) 82 | } 83 | 84 | let { endpoint } = callAPI 85 | const { schema, types } = callAPI 86 | 87 | if (typeof endpoint === 'function') { 88 | endpoint = endpoint(store.getState()) 89 | } 90 | 91 | if (typeof endpoint !== 'string') { 92 | throw new Error('Specify a string endpoint URL.') 93 | } 94 | if (!schema) { 95 | throw new Error('Specify one of the exported Schemas.') 96 | } 97 | if (!Array.isArray(types) || types.length !== 3) { 98 | throw new Error('Expected an array of three action types.') 99 | } 100 | if (!types.every(type => typeof type === 'string')) { 101 | throw new Error('Expected action types to be strings.') 102 | } 103 | 104 | const actionWith = data => { 105 | const finalAction = Object.assign({}, action, data) 106 | delete finalAction[CALL_API] 107 | return finalAction 108 | } 109 | 110 | const [ requestType, successType, failureType ] = types 111 | next(actionWith({ type: requestType })) 112 | 113 | const apiRoot = store.getState().configuration.API_ROOT 114 | 115 | return callApi(apiRoot, endpoint, schema).then( 116 | response => next(actionWith({ 117 | response, 118 | type: successType 119 | })), 120 | error => next(actionWith({ 121 | type: failureType, 122 | error: error.message || 'Something bad happened' 123 | })) 124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /examples/real-world/src/common/reducers/entities.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge' 2 | 3 | const entities = (state = { users: {}, repos: {} }, action) => { 4 | if (action.response && action.response.entities) { 5 | return merge({}, state, action.response.entities) 6 | } 7 | 8 | return state 9 | } 10 | 11 | export default entities 12 | -------------------------------------------------------------------------------- /examples/real-world/src/common/reducers/paginate.js: -------------------------------------------------------------------------------- 1 | import union from 'lodash/union' 2 | 3 | // Creates a reducer managing pagination, given the action types to handle, 4 | // and a function telling how to extract the key from an action. 5 | const paginate = ({ types, mapActionToKey }) => { 6 | if (!Array.isArray(types) || types.length !== 3) { 7 | throw new Error('Expected types to be an array of three elements.') 8 | } 9 | if (!types.every(t => typeof t === 'string')) { 10 | throw new Error('Expected types to be strings.') 11 | } 12 | if (typeof mapActionToKey !== 'function') { 13 | throw new Error('Expected mapActionToKey to be a function.') 14 | } 15 | 16 | const [ requestType, successType, failureType ] = types 17 | 18 | const updatePagination = (state = { 19 | isFetching: false, 20 | nextPageUrl: undefined, 21 | pageCount: 0, 22 | ids: [] 23 | }, action) => { 24 | switch (action.type) { 25 | case requestType: 26 | return { 27 | ...state, 28 | isFetching: true 29 | } 30 | case successType: 31 | return { 32 | ...state, 33 | isFetching: false, 34 | ids: union(state.ids, action.response.result), 35 | nextPageUrl: action.response.nextPageUrl, 36 | pageCount: state.pageCount + 1 37 | } 38 | case failureType: 39 | return { 40 | ...state, 41 | isFetching: false 42 | } 43 | default: 44 | return state 45 | } 46 | } 47 | 48 | return (state = {}, action) => { 49 | // Update pagination by key 50 | switch (action.type) { 51 | case requestType: 52 | case successType: 53 | case failureType: 54 | const key = mapActionToKey(action) 55 | if (typeof key !== 'string') { 56 | throw new Error('Expected key to be a string.') 57 | } 58 | return { 59 | ...state, 60 | [key]: updatePagination(state[key], action) 61 | } 62 | default: 63 | return state 64 | } 65 | } 66 | } 67 | 68 | export default paginate 69 | -------------------------------------------------------------------------------- /examples/real-world/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory } from 'react-router' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | import { Root, configureStore } from './root' 6 | 7 | const store = configureStore() 8 | const history = syncHistoryWithStore(browserHistory, store) 9 | 10 | render( 11 | , 12 | document.getElementById('root') 13 | ) 14 | -------------------------------------------------------------------------------- /examples/real-world/src/repoPage/RepoPage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | import { connect } from 'react-redux' 6 | import Repo from '../common/components/Repo' 7 | import User from '../common/components/User' 8 | import List from '../common/components/List' 9 | import { loadRepo, loadStargazers } from './actions' 10 | 11 | const loadData = props => { 12 | const { fullName } = props 13 | props.loadRepo(fullName, [ 'description' ]) 14 | props.loadStargazers(fullName) 15 | } 16 | 17 | class RepoPage extends Component { 18 | static propTypes = { 19 | repo: PropTypes.object, 20 | fullName: PropTypes.string.isRequired, 21 | name: PropTypes.string.isRequired, 22 | owner: PropTypes.object, 23 | stargazers: PropTypes.array.isRequired, 24 | stargazersPagination: PropTypes.object, 25 | loadRepo: PropTypes.func.isRequired, 26 | loadStargazers: PropTypes.func.isRequired 27 | } 28 | 29 | componentWillMount() { 30 | loadData(this.props) 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | if (nextProps.fullName !== this.props.fullName) { 35 | loadData(nextProps) 36 | } 37 | } 38 | 39 | handleLoadMoreClick = () => { 40 | this.props.loadStargazers(this.props.fullName, true) 41 | } 42 | 43 | renderUser(user) { 44 | return 45 | } 46 | 47 | render() { 48 | const { repo, owner, name } = this.props 49 | if (!repo || !owner) { 50 | return

Loading {name} details...

51 | } 52 | 53 | const { stargazers, stargazersPagination } = this.props 54 | return ( 55 |
56 | 58 |
59 | 64 |
65 | ) 66 | } 67 | } 68 | 69 | const mapStateToProps = (state, ownProps) => { 70 | // We need to lower case the login/name due to the way GitHub's API behaves. 71 | // Have a look at ../middleware/api.js for more details. 72 | const login = ownProps.params.login.toLowerCase() 73 | const name = ownProps.params.name.toLowerCase() 74 | 75 | const { 76 | stargazersByRepo, 77 | entities: { users, repos } 78 | } = state 79 | 80 | const fullName = `${login}/${name}` 81 | const stargazersPagination = stargazersByRepo[fullName] || { ids: [] } 82 | const stargazers = stargazersPagination.ids.map(id => users[id]) 83 | 84 | return { 85 | fullName, 86 | name, 87 | stargazers, 88 | stargazersPagination, 89 | repo: repos[fullName], 90 | owner: users[login] 91 | } 92 | } 93 | 94 | export default connect(mapStateToProps, { 95 | loadRepo, 96 | loadStargazers 97 | })(RepoPage) 98 | -------------------------------------------------------------------------------- /examples/real-world/src/repoPage/actions.js: -------------------------------------------------------------------------------- 1 | import { CALL_API, Schemas } from '../common/middleware/api' 2 | 3 | export const REPO_REQUEST = 'REPO_REQUEST' 4 | export const REPO_SUCCESS = 'REPO_SUCCESS' 5 | export const REPO_FAILURE = 'REPO_FAILURE' 6 | 7 | // Fetches a single repository from Github API. 8 | // Relies on the custom API middleware defined in ../middleware/api.js. 9 | const fetchRepo = fullName => ({ 10 | [CALL_API]: { 11 | types: [ REPO_REQUEST, REPO_SUCCESS, REPO_FAILURE ], 12 | endpoint: `repos/${fullName}`, 13 | schema: Schemas.REPO 14 | } 15 | }) 16 | 17 | // Fetches a single repository from Github API unless it is cached. 18 | // Relies on Redux Thunk middleware. 19 | export const loadRepo = (fullName, requiredFields = []) => (dispatch, getState) => { 20 | const repo = getState().entities.repos[fullName] 21 | if (repo && requiredFields.every(key => repo.hasOwnProperty(key))) { 22 | return null 23 | } 24 | 25 | return dispatch(fetchRepo(fullName)) 26 | } 27 | 28 | export const STARGAZERS_REQUEST = 'STARGAZERS_REQUEST' 29 | export const STARGAZERS_SUCCESS = 'STARGAZERS_SUCCESS' 30 | export const STARGAZERS_FAILURE = 'STARGAZERS_FAILURE' 31 | 32 | // Fetches a page of stargazers for a particular repo. 33 | // Relies on the custom API middleware defined in ../middleware/api.js. 34 | const fetchStargazers = (fullName, nextPageUrl) => ({ 35 | fullName, 36 | [CALL_API]: { 37 | types: [ STARGAZERS_REQUEST, STARGAZERS_SUCCESS, STARGAZERS_FAILURE ], 38 | endpoint: nextPageUrl, 39 | schema: Schemas.USER_ARRAY 40 | } 41 | }) 42 | 43 | // Fetches a page of stargazers for a particular repo. 44 | // Bails out if page is cached and user didn't specifically request next page. 45 | // Relies on Redux Thunk middleware. 46 | export const loadStargazers = (fullName, nextPage) => (dispatch, getState) => { 47 | const { 48 | nextPageUrl = `repos/${fullName}/stargazers`, 49 | pageCount = 0 50 | } = getState().stargazersByRepo[fullName] || {} 51 | 52 | if (pageCount > 0 && !nextPage) { 53 | return null 54 | } 55 | 56 | return dispatch(fetchStargazers(fullName, nextPageUrl)) 57 | } 58 | -------------------------------------------------------------------------------- /examples/real-world/src/repoPage/index.js: -------------------------------------------------------------------------------- 1 | import { withReducer } from 'react-redux-dynamic-reducer' 2 | 3 | import RepoPage from './RepoPage' 4 | import reducer from './reducer' 5 | 6 | export default withReducer(reducer, 'repoPage')(RepoPage) 7 | -------------------------------------------------------------------------------- /examples/real-world/src/repoPage/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import * as ActionTypes from './actions' 3 | import entities from '../common/reducers/entities' 4 | import paginate from '../common/reducers/paginate' 5 | 6 | export default combineReducers({ 7 | entities, 8 | stargazersByRepo: paginate({ 9 | mapActionToKey: action => action.fullName, 10 | types: [ 11 | ActionTypes.STARGAZERS_REQUEST, 12 | ActionTypes.STARGAZERS_SUCCESS, 13 | ActionTypes.STARGAZERS_FAILURE 14 | ] 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /examples/real-world/src/root/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Provider } from 'react-redux' 4 | import routes from './routes' 5 | import DevTools from './containers/DevTools' 6 | import { Router } from 'react-router' 7 | 8 | const Root = ({ store, history }) => ( 9 | 10 |
11 | 12 | 13 |
14 |
15 | ) 16 | 17 | Root.propTypes = { 18 | store: PropTypes.object.isRequired, 19 | history: PropTypes.object.isRequired 20 | } 21 | 22 | export default Root 23 | -------------------------------------------------------------------------------- /examples/real-world/src/root/Root.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./Root.prod') 3 | } else { 4 | module.exports = require('./Root.dev') 5 | } 6 | -------------------------------------------------------------------------------- /examples/real-world/src/root/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Provider } from 'react-redux' 4 | import { Router } from 'react-router' 5 | import routes from './routes' 6 | 7 | const Root = ({ store, history }) => ( 8 | 9 | 10 | 11 | ) 12 | 13 | Root.propTypes = { 14 | store: PropTypes.object.isRequired, 15 | history: PropTypes.object.isRequired 16 | } 17 | export default Root 18 | -------------------------------------------------------------------------------- /examples/real-world/src/root/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /examples/real-world/src/root/index.js: -------------------------------------------------------------------------------- 1 | export { default as Root } from './Root' 2 | export { default as configureStore } from './store/configureStore' 3 | -------------------------------------------------------------------------------- /examples/real-world/src/root/reducers/configuration.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | GITHUB_REPO: 'https://github.com/ioof-holdings/redux-dynamic-reducer', 3 | API_ROOT: 'https://api.github.com/' 4 | } 5 | 6 | export default (state = initialState) => state 7 | -------------------------------------------------------------------------------- /examples/real-world/src/root/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as routing } from 'react-router-redux' 3 | import configuration from './configuration' 4 | 5 | export default combineReducers({ 6 | configuration, 7 | routing 8 | }) 9 | -------------------------------------------------------------------------------- /examples/real-world/src/root/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router' 3 | import Loadable from 'react-loadable' 4 | 5 | const Loading = () =>

Loading...

6 | 7 | const loadRoute = (getPromise) => { 8 | const RouteComponent = Loadable({ 9 | loader: () => getPromise(), 10 | loading: Loading, 11 | render(loaded, props) { 12 | const Component = loaded.default; 13 | return ; 14 | } 15 | }) 16 | 17 | const LoadableRoute = (routeProps) => 18 | return LoadableRoute 19 | } 20 | 21 | export default ( 22 | import('../app'))}> 23 | import('../repoPage'))} /> 24 | import('../userPage'))} /> 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /examples/real-world/src/root/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { compose } from 'redux' 2 | import { createStore } from 'redux-dynamic-reducer' 3 | import { applyMiddleware, applyToRoot } from 'redux-subspace' 4 | import thunk from 'redux-thunk' 5 | import { createLogger } from 'redux-logger' 6 | import wormhole from 'redux-subspace-wormhole' 7 | import api from '../../common/middleware/api' 8 | import rootReducer from '../reducers' 9 | import DevTools from '../containers/DevTools' 10 | 11 | const configureStore = preloadedState => { 12 | const store = createStore( 13 | rootReducer, 14 | preloadedState, 15 | compose( 16 | applyMiddleware( 17 | thunk, 18 | api, 19 | wormhole((state) => state.configuration, 'configuration'), 20 | applyToRoot(createLogger()) 21 | ), 22 | DevTools.instrument() 23 | ) 24 | ) 25 | 26 | if (module.hot) { 27 | // Enable Webpack hot module replacement for reducers 28 | module.hot.accept('../reducers', () => { 29 | const nextRootReducer = require('../reducers').default 30 | store.replaceReducer(nextRootReducer) 31 | }) 32 | } 33 | 34 | return store 35 | } 36 | 37 | export default configureStore 38 | -------------------------------------------------------------------------------- /examples/real-world/src/root/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod') 3 | } else { 4 | module.exports = require('./configureStore.dev') 5 | } 6 | -------------------------------------------------------------------------------- /examples/real-world/src/root/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux-dynamic-reducer' 2 | import { applyMiddleware } from 'redux-subspace' 3 | import thunk from 'redux-thunk' 4 | import api from '../../common/middleware/api' 5 | import wormhole from 'redux-subspace-wormhole' 6 | import rootReducer from '../reducers' 7 | 8 | const configureStore = preloadedState => createStore( 9 | rootReducer, 10 | preloadedState, 11 | applyMiddleware( 12 | thunk, 13 | api, 14 | wormhole((state) => state.configuration, 'configuration') 15 | ) 16 | ) 17 | 18 | export default configureStore 19 | -------------------------------------------------------------------------------- /examples/real-world/src/userPage/UserPage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | import { connect } from 'react-redux' 6 | import zip from 'lodash/zip' 7 | import User from '../common/components/User' 8 | import Repo from '../common/components/Repo' 9 | import List from '../common/components/List' 10 | import { loadUser, loadStarred } from './actions' 11 | 12 | const loadData = ({ login, loadUser, loadStarred }) => { 13 | loadUser(login, [ 'name' ]) 14 | loadStarred(login) 15 | } 16 | 17 | class UserPage extends Component { 18 | static propTypes = { 19 | login: PropTypes.string.isRequired, 20 | user: PropTypes.object, 21 | starredPagination: PropTypes.object, 22 | starredRepos: PropTypes.array.isRequired, 23 | starredRepoOwners: PropTypes.array.isRequired, 24 | loadUser: PropTypes.func.isRequired, 25 | loadStarred: PropTypes.func.isRequired 26 | } 27 | 28 | componentWillMount() { 29 | loadData(this.props) 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | if (nextProps.login !== this.props.login) { 34 | loadData(nextProps) 35 | } 36 | } 37 | 38 | handleLoadMoreClick = () => { 39 | this.props.loadStarred(this.props.login, true) 40 | } 41 | 42 | renderRepo([ repo, owner ]) { 43 | return ( 44 | 48 | ) 49 | } 50 | 51 | render() { 52 | const { user, login } = this.props 53 | if (!user) { 54 | return

Loading {login}{"'s profile..."}

55 | } 56 | 57 | const { starredRepos, starredRepoOwners, starredPagination } = this.props 58 | return ( 59 |
60 | 61 |
62 | 67 |
68 | ) 69 | } 70 | } 71 | 72 | const mapStateToProps = (state, ownProps) => { 73 | // We need to lower case the login due to the way GitHub's API behaves. 74 | // Have a look at ../middleware/api.js for more details. 75 | const login = ownProps.params.login.toLowerCase() 76 | 77 | const { 78 | starredByUser, 79 | entities: { users, repos } 80 | } = state 81 | 82 | const starredPagination = starredByUser[login] || { ids: [] } 83 | const starredRepos = starredPagination.ids.map(id => repos[id]) 84 | const starredRepoOwners = starredRepos.map(repo => users[repo.owner]) 85 | 86 | return { 87 | login, 88 | starredRepos, 89 | starredRepoOwners, 90 | starredPagination, 91 | user: users[login] 92 | } 93 | } 94 | 95 | export default connect(mapStateToProps, { 96 | loadUser, 97 | loadStarred 98 | })(UserPage) 99 | -------------------------------------------------------------------------------- /examples/real-world/src/userPage/actions.js: -------------------------------------------------------------------------------- 1 | import { CALL_API, Schemas } from '../common/middleware/api' 2 | 3 | export const USER_REQUEST = 'USER_REQUEST' 4 | export const USER_SUCCESS = 'USER_SUCCESS' 5 | export const USER_FAILURE = 'USER_FAILURE' 6 | 7 | // Fetches a single user from Github API. 8 | // Relies on the custom API middleware defined in ../middleware/api.js. 9 | const fetchUser = login => ({ 10 | [CALL_API]: { 11 | types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ], 12 | endpoint: `users/${login}`, 13 | schema: Schemas.USER 14 | } 15 | }) 16 | 17 | // Fetches a single user from Github API unless it is cached. 18 | // Relies on Redux Thunk middleware. 19 | export const loadUser = (login, requiredFields = []) => (dispatch, getState) => { 20 | const user = getState().entities.users[login] 21 | if (user && requiredFields.every(key => user.hasOwnProperty(key))) { 22 | return null 23 | } 24 | 25 | return dispatch(fetchUser(login)) 26 | } 27 | 28 | export const STARRED_REQUEST = 'STARRED_REQUEST' 29 | export const STARRED_SUCCESS = 'STARRED_SUCCESS' 30 | export const STARRED_FAILURE = 'STARRED_FAILURE' 31 | 32 | // Fetches a page of starred repos by a particular user. 33 | // Relies on the custom API middleware defined in ../middleware/api.js. 34 | const fetchStarred = (login, nextPageUrl) => ({ 35 | login, 36 | [CALL_API]: { 37 | types: [ STARRED_REQUEST, STARRED_SUCCESS, STARRED_FAILURE ], 38 | endpoint: nextPageUrl, 39 | schema: Schemas.REPO_ARRAY 40 | } 41 | }) 42 | 43 | // Fetches a page of starred repos by a particular user. 44 | // Bails out if page is cached and user didn't specifically request next page. 45 | // Relies on Redux Thunk middleware. 46 | export const loadStarred = (login, nextPage) => (dispatch, getState) => { 47 | const { 48 | nextPageUrl = `users/${login}/starred`, 49 | pageCount = 0 50 | } = getState().starredByUser[login] || {} 51 | 52 | if (pageCount > 0 && !nextPage) { 53 | return null 54 | } 55 | 56 | return dispatch(fetchStarred(login, nextPageUrl)) 57 | } 58 | -------------------------------------------------------------------------------- /examples/real-world/src/userPage/index.js: -------------------------------------------------------------------------------- 1 | import { withReducer } from 'react-redux-dynamic-reducer' 2 | 3 | import UserPage from './UserPage' 4 | import reducer from './reducer' 5 | 6 | export default withReducer(reducer, 'userPage')(UserPage) 7 | -------------------------------------------------------------------------------- /examples/real-world/src/userPage/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import * as ActionTypes from './actions' 3 | import entities from '../common/reducers/entities' 4 | import paginate from '../common/reducers/paginate' 5 | 6 | export default combineReducers({ 7 | entities, 8 | starredByUser: paginate({ 9 | mapActionToKey: action => action.login, 10 | types: [ 11 | ActionTypes.STARRED_REQUEST, 12 | ActionTypes.STARRED_SUCCESS, 13 | ActionTypes.STARRED_FAILURE 14 | ] 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /examples/todos/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/todos/README.md: -------------------------------------------------------------------------------- 1 | # Todos Example 2 | 3 | This example extends [Redux's todos example](https://github.com/reactjs/redux/tree/master/examples/todos) to bundle the Todo's component and reducer together and automatically attach the state when mounter. 4 | 5 | To run the example locally: 6 | 7 | ```sh 8 | git clone https://github.com/ioof-holdings/redux-dynamic-reducer.git 9 | 10 | cd redux-dynamic-reducer/examples/todos 11 | npm install 12 | npm start 13 | ``` 14 | 15 | Or check out the [sandbox](https://codesandbox.io/s/github/ioof-holdings/redux-dynamic-reducer/tree/master/examples/todos). 16 | -------------------------------------------------------------------------------- /examples/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "2.0.2", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.0.17" 7 | }, 8 | "dependencies": { 9 | "prop-types": "^15.6.0", 10 | "react": "^16.0.0", 11 | "react-dom": "^16.0.0", 12 | "react-redux": "^5.0.6", 13 | "react-redux-dynamic-reducer": "^2.0.2", 14 | "redux": "^3.7.2", 15 | "redux-dynamic-reducer": "^2.0.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "eject": "react-scripts eject", 21 | "test": "echo 'No Tests'" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/todos/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Dynamic Reducer Todos Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/todos/src/actions/index.js: -------------------------------------------------------------------------------- 1 | let nextTodoId = 0 2 | export const addTodo = (text) => ({ 3 | type: 'ADD_TODO', 4 | id: nextTodoId++, 5 | text 6 | }) 7 | 8 | export const setVisibilityFilter = (filter) => ({ 9 | type: 'SET_VISIBILITY_FILTER', 10 | filter 11 | }) 12 | 13 | export const toggleTodo = (id) => ({ 14 | type: 'TOGGLE_TODO', 15 | id 16 | }) 17 | -------------------------------------------------------------------------------- /examples/todos/src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as actions from './index' 2 | 3 | describe('todo actions', () => { 4 | it('addTodo should create ADD_TODO action', () => { 5 | expect(actions.addTodo('Use Redux')).toEqual({ 6 | type: 'ADD_TODO', 7 | id: 0, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('setVisibilityFilter should create SET_VISIBILITY_FILTER action', () => { 13 | expect(actions.setVisibilityFilter('active')).toEqual({ 14 | type: 'SET_VISIBILITY_FILTER', 15 | filter: 'active' 16 | }) 17 | }) 18 | 19 | it('toggleTodo should create TOGGLE_TODO action', () => { 20 | expect(actions.toggleTodo(1)).toEqual({ 21 | type: 'TOGGLE_TODO', 22 | id: 1 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /examples/todos/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withReducer } from 'react-redux-dynamic-reducer' 3 | import Footer from './Footer' 4 | import AddTodo from '../containers/AddTodo' 5 | import VisibleTodoList from '../containers/VisibleTodoList' 6 | import reducer from '../reducers' 7 | 8 | const App = () => ( 9 |
10 | 11 | 12 |
13 |
14 | ) 15 | 16 | export default withReducer(reducer, 'app')(App) 17 | -------------------------------------------------------------------------------- /examples/todos/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FilterLink from '../containers/FilterLink' 3 | 4 | const Footer = () => ( 5 |

6 | Show: 7 | {" "} 8 | 9 | All 10 | 11 | {", "} 12 | 13 | Active 14 | 15 | {", "} 16 | 17 | Completed 18 | 19 |

20 | ) 21 | 22 | export default Footer 23 | -------------------------------------------------------------------------------- /examples/todos/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Link = ({ active, children, onClick }) => { 5 | if (active) { 6 | return {children} 7 | } 8 | 9 | return ( 10 | // eslint-disable-next-line 11 | { 13 | e.preventDefault() 14 | onClick() 15 | }} 16 | > 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | Link.propTypes = { 23 | active: PropTypes.bool.isRequired, 24 | children: PropTypes.node.isRequired, 25 | onClick: PropTypes.func.isRequired 26 | } 27 | 28 | export default Link 29 | -------------------------------------------------------------------------------- /examples/todos/src/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Todo = ({ onClick, completed, text }) => ( 5 |
  • 11 | {text} 12 |
  • 13 | ) 14 | 15 | Todo.propTypes = { 16 | onClick: PropTypes.func.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired 19 | } 20 | 21 | export default Todo 22 | -------------------------------------------------------------------------------- /examples/todos/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Todo from './Todo' 4 | 5 | const TodoList = ({ todos, onTodoClick }) => ( 6 |
      7 | {todos.map(todo => 8 | onTodoClick(todo.id)} 12 | /> 13 | )} 14 |
    15 | ) 16 | 17 | TodoList.propTypes = { 18 | todos: PropTypes.arrayOf(PropTypes.shape({ 19 | id: PropTypes.number.isRequired, 20 | completed: PropTypes.bool.isRequired, 21 | text: PropTypes.string.isRequired 22 | }).isRequired).isRequired, 23 | onTodoClick: PropTypes.func.isRequired 24 | } 25 | 26 | export default TodoList 27 | -------------------------------------------------------------------------------- /examples/todos/src/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { addTodo } from '../actions' 4 | 5 | let AddTodo = ({ dispatch }) => { 6 | let input 7 | 8 | return ( 9 |
    10 |
    { 11 | e.preventDefault() 12 | if (!input.value.trim()) { 13 | return 14 | } 15 | dispatch(addTodo(input.value)) 16 | input.value = '' 17 | }}> 18 | { 19 | input = node 20 | }} /> 21 | 24 |
    25 |
    26 | ) 27 | } 28 | AddTodo = connect()(AddTodo) 29 | 30 | export default AddTodo 31 | -------------------------------------------------------------------------------- /examples/todos/src/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { setVisibilityFilter } from '../actions' 3 | import Link from '../components/Link' 4 | 5 | const mapStateToProps = (state, ownProps) => ({ 6 | active: ownProps.filter === state.visibilityFilter 7 | }) 8 | 9 | const mapDispatchToProps = (dispatch, ownProps) => ({ 10 | onClick: () => { 11 | dispatch(setVisibilityFilter(ownProps.filter)) 12 | } 13 | }) 14 | 15 | const FilterLink = connect( 16 | mapStateToProps, 17 | mapDispatchToProps 18 | )(Link) 19 | 20 | export default FilterLink 21 | -------------------------------------------------------------------------------- /examples/todos/src/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { toggleTodo } from '../actions' 3 | import TodoList from '../components/TodoList' 4 | 5 | const getVisibleTodos = (todos, filter) => { 6 | switch (filter) { 7 | case 'SHOW_ALL': 8 | return todos 9 | case 'SHOW_COMPLETED': 10 | return todos.filter(t => t.completed) 11 | case 'SHOW_ACTIVE': 12 | return todos.filter(t => !t.completed) 13 | default: 14 | throw new Error('Unknown filter: ' + filter) 15 | } 16 | } 17 | 18 | const mapStateToProps = (state) => ({ 19 | todos: getVisibleTodos(state.todos, state.visibilityFilter) 20 | }) 21 | 22 | const mapDispatchToProps = { 23 | onTodoClick: toggleTodo 24 | } 25 | 26 | const VisibleTodoList = connect( 27 | mapStateToProps, 28 | mapDispatchToProps 29 | )(TodoList) 30 | 31 | export default VisibleTodoList 32 | -------------------------------------------------------------------------------- /examples/todos/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore } from 'redux-dynamic-reducer' 4 | import { Provider } from 'react-redux' 5 | 6 | const store = createStore() 7 | 8 | import('./components/App').then(module => module.default) 9 | .then(App => render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | )) 15 | -------------------------------------------------------------------------------- /examples/todos/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | import visibilityFilter from './visibilityFilter' 4 | 5 | const todoApp = combineReducers({ 6 | todos, 7 | visibilityFilter 8 | }) 9 | 10 | export default todoApp 11 | -------------------------------------------------------------------------------- /examples/todos/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | const todos = (state = [], action) => { 2 | switch (action.type) { 3 | case 'ADD_TODO': 4 | return [ 5 | ...state, 6 | { 7 | id: action.id, 8 | text: action.text, 9 | completed: false 10 | } 11 | ] 12 | case 'TOGGLE_TODO': 13 | return state.map(todo => 14 | (todo.id === action.id) 15 | ? {...todo, completed: !todo.completed} 16 | : todo 17 | ) 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | export default todos 24 | -------------------------------------------------------------------------------- /examples/todos/src/reducers/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | const visibilityFilter = (state = 'SHOW_ALL', action) => { 2 | switch (action.type) { 3 | case 'SET_VISIBILITY_FILTER': 4 | return action.filter 5 | default: 6 | return state 7 | } 8 | } 9 | 10 | export default visibilityFilter 11 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "packages": [ 4 | "packages/*", 5 | "**/examples/*" 6 | ], 7 | "version": "2.0.2" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-dynamic-reducer", 3 | "description": "Create isolated subspaces of a Redux store", 4 | "author": "Michael Peyper", 5 | "license": "BSD-3-Clause", 6 | "main": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "scripts": { 9 | "bootstrap": "lerna bootstrap", 10 | "coverage": "istanbul report --root packages --reporter html --reporter text-summary", 11 | "dist": "lerna run --parallel dist", 12 | "lint": "eslint . --ext .js --ext .jsx", 13 | "lint:fix": "eslint . --ext .js --ext .jsx --fix", 14 | "test": "lerna run test && npm run coverage", 15 | "test:watch": "lerna run --parallel test:watch" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ioof-holdings/redux-dynamic-reducer.git" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^4.10.0", 23 | "istanbul": "^0.4.5", 24 | "lerna": "^2.5.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-syntax-dynamic-import", 8 | "@babel/plugin-syntax-import-meta", 9 | "@babel/plugin-proposal-class-properties", 10 | "@babel/plugin-proposal-json-strings" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | node_modules 3 | npm-debug.log 4 | test/ 5 | docs/ 6 | examples/ 7 | .nyc_output 8 | coverage 9 | .vscode 10 | .idea 11 | .babelrc 12 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/README.md: -------------------------------------------------------------------------------- 1 | # react-redux-dynamic-reducer 2 | 3 | [![npm version](https://img.shields.io/npm/v/react-redux-dynamic-reducer.svg?style=flat-square)](https://www.npmjs.com/package/react-redux-dynamic-reducer) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-redux-dynamic-reducer.svg?style=flat-square)](https://www.npmjs.com/package/react-redux-dynamic-reducer) 5 | [![License: MIT](https://img.shields.io/npm/l/react-redux-dynamic-reducer.svg?style=flat-square)](/LICENSE.md) 6 | 7 | This library provides [React bindings](https://facebook.github.io/react/) for [redux-dynamic-reducers](/packages/redux-dynamic-reducers), enabling reducers to be attached when components are mounted. 8 | 9 | ## How to use 10 | 11 | ### 1. Create the store 12 | 13 | ```javascript 14 | import { combineReducers } from 'redux' 15 | import { createStore } from 'redux-dynamic-reducer' 16 | 17 | ... 18 | 19 | const reducer = combineReducers({ staticReducer1, staticReducer2 }) 20 | const store = createStore(reducer) 21 | ``` 22 | 23 | Please refer to the [redux-dynamic-reducer](/packages/redux-dynamic-reducer) for more details on creating the store. 24 | 25 | ### 2. Combine a component with a reducer 26 | 27 | The `withReducer` higher-order component (HOC) can be used to bundle a reducer into a component that will automatically be attached into the store when mounted. This method will also mount the component within a [subspace](https://github.com/ioof-holdings/redux-subspace) for easy access to it's reducer. Please refer to the [redux-subspace documentation](https://github.com/ioof-holdings/redux-subspace/docs) for configuring the subspace to work with any middleware, enhancers or other extensions you are using. 28 | 29 | ```javascript 30 | import { withReducer } from 'react-redux-dynamic-reducer' 31 | 32 | export default withReducer(myReducer, 'defaultKey')(MyComponent) 33 | ``` 34 | 35 | ### 3. Mount the component 36 | 37 | ```javascript 38 | import MyComponent from './MyComponent' 39 | 40 | 41 | ... 42 | 43 | ... 44 | 45 | ``` 46 | 47 | Multiple instances of the same component can be added, as long as the have unique instance identifiers: 48 | 49 | ```javascript 50 | import MyComponent from './MyComponent' 51 | 52 | ... 53 | 54 | const MyComponent1 = MyComponent.createInstance('myInstance1') 55 | const MyComponent2 = MyComponent.createInstance('myInstance2') 56 | 57 | ... 58 | 59 | 60 | 61 | 62 | 63 | ``` 64 | 65 | Additional state can be mapped for the component or an instance of the component my providing an additional mapper: 66 | 67 | ```javascript 68 | export default withReducer(myReducer, 'defaultKey', { mapExtraState: (state, rootState) => ({ /* ... */ }) })(MyComponent) 69 | 70 | ... 71 | 72 | const MyComponentInstance = MyComponent 73 | .createInstance('instance') 74 | .withExtraState((state, rootState) => ({ /* ... */ }) }) 75 | 76 | ... 77 | 78 | const MyComponentInstance = MyComponent 79 | .createInstance('instance') 80 | .withOptions({ mapExtraState: (state, rootState) => ({ /* ... */ }) }) 81 | ``` 82 | 83 | The extra state is merged with the bundled reducer's state. 84 | 85 | By default, the components are [namespaced](https://github.com/ioof-holdings/redux-subspace#namespacing). If namespacing is not wanted for a component or and instance of the component, an options object can be provided to prevent it: 86 | 87 | ```javascript 88 | export default withReducer(myReducer, 'defaultKey', { namespaceActions: false })(MyComponent) 89 | 90 | ... 91 | 92 | const MyComponentInstance = MyComponent.createInstance('instance').withOptions({ namespaceActions: false }) 93 | ``` 94 | 95 | Combined components and reducers can be nested as deep as required, but note, the nested reducer will appear at the top level of the redux state. 96 | 97 | ## Examples 98 | 99 | Examples can be found [here](/examples#react-redux-dynamic-reducer). 100 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-dynamic-reducer", 3 | "version": "2.0.2", 4 | "description": "react-redux Provider extension for redux-subspace", 5 | "author": "Michael Peyper", 6 | "license": "BSD-3-Clause", 7 | "main": "lib/index.js", 8 | "typings": "lib/index.d.ts", 9 | "scripts": { 10 | "dist": "babel src --out-dir lib --copy-files", 11 | "lint": "eslint . --ext .js --ext .jsx", 12 | "lint:fix": "eslint . --ext .js --ext .jsx --fix", 13 | "test": "nyc mocha --compilers js:@babel/register --recursive --require jsdom-global/register --require ./test/setup.js", 14 | "test:watch": "npm test -- --watch", 15 | "prepublish": "npm run dist" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ioof-holdings/redux-dynamic-reducer.git" 20 | }, 21 | "dependencies": { 22 | "hoist-non-react-statics": "^2.3.1", 23 | "lodash.isplainobject": "^4.0.6", 24 | "prop-types": "^15.6.0", 25 | "react-redux-subspace": "^2.0.8", 26 | "recompose": "^0.26.0", 27 | "redux-subspace": "^2.0.8" 28 | }, 29 | "peerDependencies": { 30 | "react": "^15.1 || ^16.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.0.0", 34 | "@babel/core": "^7.0.0", 35 | "@babel/plugin-proposal-class-properties": "^7.0.0", 36 | "@babel/plugin-proposal-json-strings": "^7.0.0", 37 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 38 | "@babel/plugin-syntax-import-meta": "^7.0.0", 39 | "@babel/preset-env": "^7.0.0", 40 | "@babel/preset-react": "^7.0.0", 41 | "@babel/register": "^7.0.0", 42 | "@types/react": "^16.0.20", 43 | "babel-eslint": "^8.0.1", 44 | "chai": "^4.1.2", 45 | "enzyme": "^3.1.1", 46 | "enzyme-adapter-react-16": "^1.0.4", 47 | "eslint": "^4.10.0", 48 | "eslint-plugin-react": "^7.4.0", 49 | "jsdom": "^11.3.0", 50 | "jsdom-global": "^3.0.2", 51 | "mocha": "^4.0.1", 52 | "nyc": "^11.3.0", 53 | "react": "^16.0.0", 54 | "react-dom": "^16.0.0", 55 | "react-redux": "^5.0.6", 56 | "react-test-renderer": "^16.0.0", 57 | "redux": "^3.7.2", 58 | "redux-mock-store": "^1.3.0", 59 | "sinon": "^4.1.1", 60 | "sinon-chai": "^2.14.0", 61 | "typescript": "^2.6.1", 62 | "typescript-definition-tester": "0.0.5" 63 | }, 64 | "nyc": { 65 | "temp-directory": "coverage/.nyc_output", 66 | "reporter": [ 67 | "html", 68 | "text", 69 | "json" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import * as React from 'react'; 10 | import { Reducer } from 'redux'; 11 | import { MapState } from 'redux-subspace'; 12 | 13 | interface Options { 14 | namespaceActions?: Boolean; 15 | mapExtraState?: MapState; 16 | } 17 | 18 | interface ComponentWithReducer extends React.ComponentClass, React.StatelessComponent { 19 | createInstance(identifier: string): ComponentWithReducer, 20 | withExtraState(mapExtraState: MapState): ComponentWithReducer 21 | withExtraState(mapExtraState: MapState): ComponentWithReducer 22 | withOptions(options: Options): ComponentWithReducer 23 | } 24 | 25 | interface ComponentDecorator { 26 | (component: React.ComponentClass | React.StatelessComponent): ComponentWithReducer; 27 | } 28 | 29 | export interface ComponentEnhancer { 30 | (reducer: Reducer, identifier: string, options?: Options): ComponentDecorator; 31 | } 32 | 33 | export const withReducer: ComponentEnhancer; 34 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export { default as withReducer } from './withReducer' 10 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/src/withReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react' 10 | import PropTypes from 'prop-types' 11 | import hoistNonReactStatics from 'hoist-non-react-statics'; 12 | import wrapDisplayName from 'recompose/wrapDisplayName' 13 | import { namespaced } from 'redux-subspace' 14 | import { subspaced } from 'react-redux-subspace' 15 | import isPlainObject from 'lodash.isplainobject' 16 | 17 | const withReducer = ( 18 | bundledReducer, 19 | identifier, 20 | { 21 | namespaceActions = true, 22 | mapExtraState = () => undefined 23 | } = {} 24 | ) => { 25 | let namespace = undefined 26 | let reducer = bundledReducer 27 | const options = { 28 | namespaceActions, 29 | mapExtraState 30 | } 31 | 32 | if (options.namespaceActions) { 33 | namespace = identifier 34 | reducer = namespaced(namespace)(bundledReducer) 35 | } 36 | 37 | const mapState = (state, rootState) => { 38 | const componentState = state[identifier] 39 | const extraState = mapExtraState(state, rootState) 40 | return isPlainObject(componentState) && isPlainObject(extraState) 41 | ? { ...extraState, ...componentState } 42 | : componentState 43 | } 44 | 45 | const subspaceEnhancer = subspaced(mapState, namespace) 46 | 47 | return (WrappedComponent) => { 48 | const Component = subspaceEnhancer(WrappedComponent) 49 | 50 | class ComponentWithReducer extends React.PureComponent { 51 | 52 | constructor(props, context) { 53 | super(props, context) 54 | 55 | const storeNamespace = this.context.store.namespace 56 | this.state = { 57 | identifier: storeNamespace ? `${storeNamespace}/${identifier}` : identifier, 58 | namespacer: namespaced(this.context.store.namespace) 59 | } 60 | } 61 | 62 | componentWillMount() { 63 | const attachReducersCheck = typeof this.context.store.attachReducers === 'function' 64 | 65 | if (process.env.NODE_ENV !== 'production') { 66 | console.assert(attachReducersCheck, `'store.attachReducers' function is missing: Unable to attach reducer '${identifier}' into the store.`) 67 | } 68 | 69 | if (attachReducersCheck) { 70 | const { identifier, namespacer } = this.state 71 | this.context.store.attachReducers({ [identifier]: namespacer(reducer) }) 72 | } 73 | } 74 | 75 | render() { 76 | return 77 | } 78 | } 79 | 80 | hoistNonReactStatics(ComponentWithReducer, Component) 81 | 82 | ComponentWithReducer.displayName = wrapDisplayName(WrappedComponent, `ComponentWithReducer(${identifier})`) 83 | 84 | ComponentWithReducer.contextTypes = { 85 | store: PropTypes.object 86 | } 87 | 88 | ComponentWithReducer.createInstance = (instanceIdentfier) => { 89 | return withReducer(bundledReducer, instanceIdentfier, options)(WrappedComponent) 90 | } 91 | 92 | ComponentWithReducer.withExtraState = (mapExtraState) => { 93 | return withReducer(bundledReducer, identifier, { ...options, mapExtraState })(WrappedComponent) 94 | } 95 | 96 | ComponentWithReducer.withOptions = (overrideOptions) => { 97 | return withReducer(bundledReducer, identifier, { ...options, ...overrideOptions })(WrappedComponent) 98 | } 99 | 100 | return ComponentWithReducer 101 | } 102 | } 103 | 104 | export default withReducer 105 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import babelRegister from 'babel-register' 10 | import Enzyme from 'enzyme' 11 | import Adapter from 'enzyme-adapter-react-16' 12 | 13 | babelRegister() 14 | 15 | global.expect = require('chai').expect 16 | global.sinon = require('sinon') 17 | 18 | var chai = require("chai") 19 | var sinonChai = require("sinon-chai") 20 | global.expect = chai.expect 21 | global.assert = chai.assert 22 | chai.use(sinonChai) 23 | 24 | Enzyme.configure({ adapter: new Adapter() }) 25 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/test/typescript/definitions-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import * as ts from 'typescript' 10 | import * as tt from 'typescript-definition-tester' 11 | import path from 'path' 12 | import fs from 'fs' 13 | 14 | describe('TypeScript definitions', function () { 15 | 16 | const options = { 17 | noEmitOnError: true, 18 | noImplicitAny: true, 19 | target: ts.ScriptTarget.ES5, 20 | module: ts.ModuleKind.CommonJS, 21 | jsx: ts.JsxEmit.React 22 | } 23 | 24 | fs.readdirSync(path.join(__dirname, 'definitions')).forEach((filename) => { 25 | it(`should compile ${path.basename(filename, path.extname(filename))} against index.d.ts`, (done) => { 26 | tt.compile([path.join(__dirname, 'definitions', filename)], options, done) 27 | }).timeout(20000) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/test/typescript/definitions/withReducer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import * as React from 'react' 10 | import { withReducer } from '../../../src' 11 | 12 | const reducer = (state = {}) => state 13 | 14 | class MyStandardComponent extends React.Component { 15 | render() { 16 | return
    ; 17 | } 18 | } 19 | 20 | const MyStatelessComponent = () => { 21 | return ( 22 |
    23 | ) 24 | } 25 | 26 | const DecoratedStandardComponent = withReducer(reducer, 'standard')(MyStandardComponent) 27 | const DecoratedStatelessComponent = withReducer(reducer, 'stateless')(MyStatelessComponent) 28 | 29 | const DecoratedInstanceOfStandardComponent = withReducer(reducer, 'standard')(MyStandardComponent).createInstance('instance') 30 | const DecoratedInstanceOfStatelessComponent = withReducer(reducer, 'stateless')(MyStatelessComponent).createInstance('instance') 31 | 32 | const DecoratedStandardComponentWithExtraState = withReducer(reducer, 'standard')(MyStandardComponent).withExtraState((state: {}) => ({ test: "value" })) 33 | const DecoratedStatelessComponentWithExtraState = withReducer(reducer, 'standard')(MyStatelessComponent).withExtraState((state: {}) => ({ test: "value" })) 34 | 35 | const DecoratedStandardComponentWithExtraAndRootState = withReducer(reducer, 'standard')(MyStandardComponent).withExtraState((state: {}, rootState: {}) => ({ test: "value" })) 36 | const DecoratedStatelessComponentWithExtraAndRootState = withReducer(reducer, 'standard')(MyStatelessComponent).withExtraState((state: {}, rootState: {}) => ({ test: "value" })) 37 | 38 | const DecoratedStandardComponentWithDefaultOptions = withReducer(reducer, 'standard', { namespaceActions: false, mapExtraState: (state: {}, rootState: {}) => ({ test: "value" }) })(MyStandardComponent) 39 | const DecoratedStatelessComponentWithDefaultOptions = withReducer(reducer, 'stateless', { namespaceActions: false, mapExtraState: (state: {}, rootState: {}) => ({ test: "value" }) })(MyStatelessComponent) 40 | 41 | const DecoratedStandardComponentWithEmptyOptions = withReducer(reducer, 'standard', {})(MyStandardComponent) 42 | const DecoratedStatelessComponentWithEmptyOptions = withReducer(reducer, 'stateless', {})(MyStatelessComponent) 43 | 44 | const DecoratedStandardComponentWithInstanceOptions = withReducer(reducer, 'standard')(MyStandardComponent).withOptions({ namespaceActions: false }) 45 | const DecoratedStatelessComponentWithInstanceOptions = withReducer(reducer, 'stateless')(MyStatelessComponent).withOptions({ namespaceActions: false }) 46 | 47 | const TestInJSX = () => ( 48 |
    49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    64 | ) 65 | -------------------------------------------------------------------------------- /packages/react-redux-dynamic-reducer/test/withReducer-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react' 10 | import { Provider, connect } from 'react-redux' 11 | import configureStore from 'redux-mock-store' 12 | import { render } from 'enzyme' 13 | import withReducer from '../src/withReducer' 14 | 15 | describe('withReducer Tests', () => { 16 | 17 | const reducer = (state = 'reducer state') => state 18 | 19 | describe('component defaults', () => { 20 | it('should wrap standard component', () => { 21 | class TestComponent extends React.Component { 22 | render() { 23 | return ( 24 |

    {this.props.value}

    25 | ) 26 | } 27 | } 28 | 29 | const mockStore = configureStore()({}) 30 | mockStore.attachReducers = sinon.spy() 31 | 32 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent) 33 | 34 | let testComponent = render( 35 | 36 | 37 | 38 | ) 39 | 40 | expect(testComponent.text()).to.equal('expected') 41 | }) 42 | 43 | it('should wrap stateless component', () => { 44 | const TestComponent = ({ value }) =>

    {value}

    45 | 46 | const mockStore = configureStore()({}) 47 | mockStore.attachReducers = sinon.spy() 48 | 49 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent) 50 | 51 | let testComponent = render( 52 | 53 | 54 | 55 | ) 56 | 57 | expect(testComponent.text()).to.equal('expected') 58 | }) 59 | 60 | it('should wrap redux connected component', () => { 61 | const TestComponent = ({ value }) =>

    {value}

    62 | const ConnectedComponent = connect(state => ({value: state.value}))(TestComponent) 63 | 64 | const mockStore = configureStore()({ default: { value: "expected" }}) 65 | mockStore.attachReducers = sinon.spy() 66 | 67 | let DecoratedComponent = withReducer(reducer, 'default')(ConnectedComponent) 68 | 69 | let testComponent = render( 70 | 71 | 72 | 73 | ) 74 | 75 | expect(testComponent.text()).to.equal('expected') 76 | }) 77 | 78 | it('should namespace component as default', () => { 79 | const TestComponent = ({ value }) =>

    {value}

    80 | 81 | const reducerWithAction = (state = {}, action) => { 82 | switch(action.type) { 83 | case "TEST": 84 | return action.payload 85 | default: 86 | return state 87 | } 88 | } 89 | 90 | const mockStore = configureStore()({}) 91 | mockStore.attachReducers = sinon.spy() 92 | 93 | let DecoratedComponent = withReducer(reducerWithAction, 'default')(TestComponent) 94 | 95 | render( 96 | 97 | 98 | 99 | ) 100 | 101 | expect(mockStore.attachReducers.args[0][0].default("expected", { type: "TEST", payload: "wrong"})).to.equal("expected") 102 | }) 103 | 104 | it('should namespace component', () => { 105 | const TestComponent = ({ value }) =>

    {value}

    106 | 107 | const reducerWithAction = (state = {}, action) => { 108 | switch(action.type) { 109 | case "TEST": 110 | return action.payload 111 | default: 112 | return state 113 | } 114 | } 115 | 116 | const mockStore = configureStore()({}) 117 | mockStore.attachReducers = sinon.spy() 118 | 119 | let DecoratedComponent = withReducer(reducerWithAction, 'default', { namespace: true })(TestComponent) 120 | 121 | render( 122 | 123 | 124 | 125 | ) 126 | 127 | expect(mockStore.attachReducers.args[0][0].default("expected", { type: "TEST", payload: "wrong"})).to.equal("expected") 128 | }) 129 | 130 | it('should not namespace component', () => { 131 | const TestComponent = ({ value }) =>

    {value}

    132 | 133 | const reducerWithAction = (state = {}, action) => { 134 | switch(action.type) { 135 | case "TEST": 136 | return action.payload 137 | default: 138 | return state 139 | } 140 | } 141 | 142 | const mockStore = configureStore()({}) 143 | mockStore.attachReducers = sinon.spy() 144 | 145 | let DecoratedComponent = withReducer(reducerWithAction, 'default', { namespaceActions: false })(TestComponent) 146 | 147 | render( 148 | 149 | 150 | 151 | ) 152 | 153 | expect(mockStore.attachReducers.args[0][0].default("wrong", { type: "TEST", payload: "expected"})).to.equal("expected") 154 | }) 155 | 156 | it('should map extra state for component', () => { 157 | const TestComponent = ({ value, otherValue }) =>

    {value} - {otherValue}

    158 | const ConnectedComponent = connect(state => ({ value: state.value, otherValue: state.otherValue }))(TestComponent) 159 | 160 | const mockStore = configureStore()({ default: { value: "expected" }, otherValue: "other"}) 161 | mockStore.attachReducers = sinon.spy() 162 | 163 | let DecoratedComponent = withReducer(reducer, 'default')(ConnectedComponent) 164 | .withExtraState((state, rootState) => ({ otherValue: rootState.otherValue })) 165 | 166 | let testComponent = render( 167 | 168 | 169 | 170 | ) 171 | 172 | expect(testComponent.text()).to.equal('expected - other') 173 | }) 174 | 175 | it('should map extra state from options component', () => { 176 | const TestComponent = ({ value, otherValue }) =>

    {value} - {otherValue}

    177 | const ConnectedComponent = connect(state => ({ value: state.value, otherValue: state.otherValue }))(TestComponent) 178 | 179 | const mockStore = configureStore()({ default: { value: "expected" }, otherValue: "other" }) 180 | mockStore.attachReducers = sinon.spy() 181 | 182 | let DecoratedComponent = withReducer(reducer, 'default', { mapExtraState: (state, rootState) => ({ otherValue: rootState.otherValue }) })(ConnectedComponent) 183 | 184 | let testComponent = render( 185 | 186 | 187 | 188 | ) 189 | 190 | expect(testComponent.text()).to.equal('expected - other') 191 | }) 192 | 193 | it('should override options for component', () => { 194 | const TestComponent = ({ value }) =>

    {value}

    195 | 196 | const mockStore = configureStore()() 197 | mockStore.attachReducers = sinon.spy() 198 | 199 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent).withOptions({}) 200 | 201 | let testComponent = render( 202 | 203 | 204 | 205 | ) 206 | 207 | expect(testComponent.text()).to.equal('expected') 208 | }) 209 | 210 | it('should wrap nested components', () => { 211 | 212 | const nestedReducer = (state = 'nested reducer state') => state 213 | 214 | const mockStore = configureStore()({ default1: { value: "wrong", default2: { value: "expected" } } }) 215 | mockStore.attachReducers = sinon.spy() 216 | 217 | let DecoratedComponent1 = withReducer(nestedReducer, 'default2')(connect(state => ({value: state.value}))(({ value }) =>

    {value}

    )) 218 | let DecoratedComponent2 = withReducer(reducer, 'default1')(() => ) 219 | 220 | let testComponent = render( 221 | 222 | 223 | 224 | ) 225 | 226 | expect(testComponent.text()).to.equal('expected') 227 | expect(mockStore.attachReducers.args[0][0].default1()).to.equal('reducer state') 228 | expect(mockStore.attachReducers.args[1][0]['default1/default2']()).to.equal('nested reducer state') 229 | }) 230 | 231 | it('should raise error if store cannot have reducers attached', () => { 232 | const TestComponent = ({ value }) =>

    {value}

    233 | 234 | const mockStore = configureStore()({}) 235 | 236 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent) 237 | 238 | expect(() => render( 239 | 240 | 241 | 242 | )).to.throw("store.attachReducers' function is missing: Unable to attach reducer 'default' into the store.") 243 | }) 244 | 245 | it('should not raise error if store cannot have reducers attached in production', () => { 246 | const nodeEnv = process.env.NODE_ENV 247 | 248 | try { 249 | process.env.NODE_ENV = 'production' 250 | 251 | const TestComponent = ({ value }) =>

    {value}

    252 | 253 | const mockStore = configureStore()({}) 254 | 255 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent) 256 | 257 | let testComponent = render( 258 | 259 | 260 | 261 | ) 262 | 263 | expect(testComponent.text()).to.equal('expected') 264 | } finally { 265 | process.env.NODE_ENV = nodeEnv 266 | } 267 | }) 268 | }) 269 | 270 | describe('component instance', () => { 271 | it('should wrap standard component', () => { 272 | class TestComponent extends React.Component { 273 | render() { 274 | return ( 275 |

    {this.props.value}

    276 | ) 277 | } 278 | } 279 | 280 | const mockStore = configureStore()({}) 281 | mockStore.attachReducers = sinon.spy() 282 | 283 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent).createInstance('instance') 284 | 285 | let testComponent = render( 286 | 287 | 288 | 289 | ) 290 | 291 | expect(testComponent.text()).to.equal('expected') 292 | }) 293 | 294 | it('should wrap stateless component', () => { 295 | const TestComponent = ({ value }) =>

    {value}

    296 | 297 | const mockStore = configureStore()({}) 298 | mockStore.attachReducers = sinon.spy() 299 | 300 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent).createInstance('instance') 301 | 302 | let testComponent = render( 303 | 304 | 305 | 306 | ) 307 | 308 | expect(testComponent.text()).to.equal('expected') 309 | }) 310 | 311 | it('should wrap redux connected component', () => { 312 | const TestComponent = ({ value }) =>

    {value}

    313 | const ConnectedComponent = connect(state => ({value: state.value}))(TestComponent) 314 | 315 | const mockStore = configureStore()({ instance: { value: "expected" }}) 316 | mockStore.attachReducers = sinon.spy() 317 | 318 | let DecoratedComponent = withReducer(reducer, 'default')(ConnectedComponent).createInstance('instance') 319 | 320 | let testComponent = render( 321 | 322 | 323 | 324 | ) 325 | 326 | expect(testComponent.text()).to.equal('expected') 327 | }) 328 | 329 | it('should namespace component as default', () => { 330 | const TestComponent = ({ value }) =>

    {value}

    331 | 332 | const reducerWithAction = (state = {}, action) => { 333 | switch(action.type) { 334 | case "TEST": 335 | return action.payload 336 | default: 337 | return state 338 | } 339 | } 340 | 341 | const mockStore = configureStore()({}) 342 | mockStore.attachReducers = sinon.spy() 343 | 344 | let DecoratedComponent = withReducer(reducerWithAction, 'default')(TestComponent).createInstance('instance') 345 | 346 | render( 347 | 348 | 349 | 350 | ) 351 | 352 | expect(mockStore.attachReducers.args[0][0].instance("expected", { type: "TEST", payload: "wrong"})).to.equal("expected") 353 | }) 354 | 355 | it('should namespace component', () => { 356 | const TestComponent = ({ value }) =>

    {value}

    357 | 358 | const reducerWithAction = (state = {}, action) => { 359 | switch(action.type) { 360 | case "TEST": 361 | return action.payload 362 | default: 363 | return state 364 | } 365 | } 366 | 367 | const mockStore = configureStore()({}) 368 | mockStore.attachReducers = sinon.spy() 369 | 370 | let DecoratedComponent = withReducer(reducerWithAction, 'default', { namespace: true })(TestComponent).createInstance('instance') 371 | 372 | render( 373 | 374 | 375 | 376 | ) 377 | 378 | expect(mockStore.attachReducers.args[0][0].instance("expected", { type: "TEST", payload: "wrong"})).to.equal("expected") 379 | }) 380 | 381 | it('should not namespace component', () => { 382 | const TestComponent = ({ value }) =>

    {value}

    383 | 384 | const reducerWithAction = (state = {}, action) => { 385 | switch(action.type) { 386 | case "TEST": 387 | return action.payload 388 | default: 389 | return state 390 | } 391 | } 392 | 393 | const mockStore = configureStore()({}) 394 | mockStore.attachReducers = sinon.spy() 395 | 396 | let DecoratedComponent = withReducer(reducerWithAction, 'default', { namespaceActions: false })(TestComponent).createInstance('instance') 397 | 398 | render( 399 | 400 | 401 | 402 | ) 403 | 404 | expect(mockStore.attachReducers.args[0][0].instance("wrong", { type: "TEST", payload: "expected"})).to.equal("expected") 405 | }) 406 | 407 | it('should map extra state for component', () => { 408 | const TestComponent = ({ value, otherValue }) =>

    {value} - {otherValue}

    409 | const ConnectedComponent = connect(state => ({ value: state.value, otherValue: state.otherValue }))(TestComponent) 410 | 411 | const mockStore = configureStore()({ instance: { value: "expected" }, otherValue: "other" }) 412 | mockStore.attachReducers = sinon.spy() 413 | 414 | let DecoratedComponent = withReducer(reducer, 'default')(ConnectedComponent) 415 | .createInstance('instance') 416 | .withExtraState((state, rootState) => ({ otherValue: rootState.otherValue })) 417 | 418 | let testComponent = render( 419 | 420 | 421 | 422 | ) 423 | 424 | expect(testComponent.text()).to.equal('expected - other') 425 | }) 426 | 427 | it('should map extra state from options component', () => { 428 | const TestComponent = ({ value, otherValue }) =>

    {value} - {otherValue}

    429 | const ConnectedComponent = connect(state => ({ value: state.value, otherValue: state.otherValue }))(TestComponent) 430 | 431 | const mockStore = configureStore()({ instance: { value: "expected" }, otherValue: "other" }) 432 | mockStore.attachReducers = sinon.spy() 433 | 434 | let DecoratedComponent = withReducer(reducer, 'default', { mapExtraState: (state, rootState) => ({ otherValue: rootState.otherValue }) })(ConnectedComponent) 435 | .createInstance('instance') 436 | 437 | let testComponent = render( 438 | 439 | 440 | 441 | ) 442 | 443 | expect(testComponent.text()).to.equal('expected - other') 444 | }) 445 | 446 | it('should override options for component', () => { 447 | const TestComponent = ({ value }) =>

    {value}

    448 | 449 | const mockStore = configureStore()() 450 | mockStore.attachReducers = sinon.spy() 451 | 452 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent) 453 | .createInstance('instance') 454 | .withOptions({}) 455 | 456 | let testComponent = render( 457 | 458 | 459 | 460 | ) 461 | 462 | expect(testComponent.text()).to.equal('expected') 463 | }) 464 | 465 | it('should wrap nested components', () => { 466 | 467 | const nestedReducer = (state = 'nested reducer state') => state 468 | 469 | const mockStore = configureStore()({ instance1: { value: "wrong", instance2: { value: "expected" } } }) 470 | mockStore.attachReducers = sinon.spy() 471 | 472 | let DecoratedComponent1 = withReducer(nestedReducer, 'default2')(connect(state => ({value: state.value}))(({ value }) =>

    {value}

    )).createInstance('instance2') 473 | let DecoratedComponent2 = withReducer(reducer, 'default1')(() => ).createInstance('instance1') 474 | 475 | let testComponent = render( 476 | 477 | 478 | 479 | ) 480 | 481 | expect(testComponent.text()).to.equal('expected') 482 | expect(mockStore.attachReducers.args[0][0].instance1()).to.equal('reducer state') 483 | expect(mockStore.attachReducers.args[1][0]['instance1/instance2']()).to.equal('nested reducer state') 484 | }) 485 | 486 | it('should raise error if store cannot have reducers attached', () => { 487 | const TestComponent = ({ value }) =>

    {value}

    488 | 489 | const mockStore = configureStore()({}) 490 | 491 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent).createInstance('instance') 492 | 493 | expect(() => render( 494 | 495 | 496 | 497 | )).to.throw("store.attachReducers' function is missing: Unable to attach reducer 'instance' into the store.") 498 | }) 499 | 500 | it('should not raise error if store cannot have reducers attached in production', () => { 501 | const nodeEnv = process.env.NODE_ENV 502 | 503 | try { 504 | process.env.NODE_ENV = 'production' 505 | 506 | const TestComponent = ({ value }) =>

    {value}

    507 | 508 | const mockStore = configureStore()({}) 509 | 510 | let DecoratedComponent = withReducer(reducer, 'default')(TestComponent).createInstance('instance') 511 | 512 | let testComponent = render( 513 | 514 | 515 | 516 | ) 517 | 518 | expect(testComponent.text()).to.equal('expected') 519 | } finally { 520 | process.env.NODE_ENV = nodeEnv 521 | } 522 | }) 523 | }) 524 | }) -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-syntax-dynamic-import", 8 | "@babel/plugin-syntax-import-meta", 9 | "@babel/plugin-proposal-class-properties", 10 | "@babel/plugin-proposal-json-strings" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | node_modules 3 | npm-debug.log 4 | test/ 5 | docs/ 6 | examples/ 7 | .nyc_output 8 | coverage 9 | .vscode 10 | .idea 11 | .babelrc 12 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/README.md: -------------------------------------------------------------------------------- 1 | # redux-dynamic-reducer 2 | 3 | [![npm version](https://img.shields.io/npm/v/redux-dynamic-reducer.svg?style=flat-square)](https://www.npmjs.com/package/redux-dynamic-reducer) 4 | [![npm downloads](https://img.shields.io/npm/dm/redux-dynamic-reducer.svg?style=flat-square)](https://www.npmjs.com/package/redux-dynamic-reducer) 5 | [![License: MIT](https://img.shields.io/npm/l/redux-dynamic-reducer.svg?style=flat-square)](/LICENSE.md) 6 | 7 | This is a library to create [Redux](http://redux.js.org/) stores that can have additional reducers dynamically attached at runtime. 8 | 9 | It is based on an example proposed by Dan Abramov in a [StackOverflow answer](http://stackoverflow.com/questions/32968016/how-to-dynamically-load-reducers-for-code-splitting-in-a-redux-application#33044701). 10 | 11 | ## What it does 12 | 13 | Redux only supports a single root reducer for the store. When designing the store structure, [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) can be used to combine multiple reducers into a single reducer for the store. However, you cannot add more reducers to the combination after the store has been created. 14 | 15 | That's where this library fits in. It allows you to provide a single root reducer but also provide additional reducers to be merged into the root reducer after the store is created. 16 | 17 | It also provides a useful utilities to package a component with a related reducer and attach it when the component is mounted. 18 | 19 | ## When to use it 20 | 21 | * You do not know which reducers are required when creating the store. 22 | * You want to split your bundle and some reducers will only be available after the import is resolved. 23 | 24 | ## How to use 25 | 26 | ### 1. Create the store 27 | 28 | The `createStore` function can be used to create store that can have reducer dynamically attached. It is a drop-in replacement for the [built-in Redux version](http://redux.js.org/docs/api/createStore.html): 29 | 30 | ```javascript 31 | import { combineReducers } from 'redux' 32 | import { createStore } from 'redux-dynamic-reducer' 33 | 34 | ... 35 | 36 | const reducer = combineReducers({ staticReducer1, staticReducer2 }) 37 | const store = createStore(reducer) 38 | ``` 39 | 40 | #### Initial State and Middleware 41 | 42 | The `createStore` function also supports all of the optional parameters that the [built-in Redux `createStore` function](http://redux.js.org/docs/api/createStore.html) does: 43 | 44 | ```javascript 45 | const store = createStore(reducer, { initial: 'state' }) 46 | ``` 47 | 48 | ```javascript 49 | const store = createStore(reducer, applyMiddleware(middleware)) 50 | ``` 51 | 52 | ```javascript 53 | const store = createStore(reducer, { initial: 'state' }, applyMiddleware(middleware)) 54 | ``` 55 | 56 | ### 2. Add a dynamic reducer 57 | 58 | The store now has a new function on it caller `attachReducers`: 59 | 60 | ```javascript 61 | store.attachReducers({ dynamicReducer }) 62 | ``` 63 | 64 | Multiple reducers can be attached as well: 65 | 66 | ```javascript 67 | store.attachReducers({ dynamicReducer1, dynamicReducer2 }) 68 | ``` 69 | 70 | Reducers can also be added to nested locations in the store: 71 | 72 | ```javascript 73 | store.attachReducers({ 74 | some: { 75 | path: { 76 | to: { 77 | dynamicReducer 78 | } 79 | } 80 | } 81 | } ) 82 | ``` 83 | 84 | ```javascript 85 | store.attachReducers({ 'some.path.to': { dynamicReducer } } } }) 86 | ``` 87 | 88 | ```javascript 89 | store.attachReducers({ 'some/path/to': { dynamicReducer } } } }) 90 | ``` 91 | 92 | ## Examples 93 | 94 | Examples can be found [here](/examples#redux-dynamic-reducer). 95 | 96 | ## Limitations 97 | 98 | * Each dynamic reducer needs a unique key 99 | * If the same key is used in a subsequent attachment, the original reducer will be replaced 100 | * Nested reducers cannot be attached to nodes of the state tree owned by a static reducer 101 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-dynamic-reducer", 3 | "version": "2.0.2", 4 | "description": "Create Redux stores that can have additional reducers dynamically attached at runtime", 5 | "author": "Michael Peyper", 6 | "license": "BSD-3-Clause", 7 | "main": "lib/index.js", 8 | "typings": "lib/index.d.ts", 9 | "scripts": { 10 | "dist": "babel src --out-dir lib --copy-files", 11 | "lint": "eslint . --ext .js --ext .jsx", 12 | "lint:fix": "eslint . --ext .js --ext .jsx --fix", 13 | "test": "nyc mocha --compilers js:@babel/register --require ./test/setup.js $(find test -name '*-spec.js')", 14 | "test:watch": "npm test -- --watch", 15 | "prepublish": "npm run dist" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ioof-holdings/redux-dynamic-reducer.git" 20 | }, 21 | "dependencies": { 22 | "lodash.isplainobject": "^4.0.6", 23 | "redux-concatenate-reducers": "^1.0.0" 24 | }, 25 | "peerDependencies": { 26 | "redux": "^3.0.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.0.0", 30 | "@babel/core": "^7.0.0", 31 | "@babel/plugin-proposal-class-properties": "^7.0.0", 32 | "@babel/plugin-proposal-json-strings": "^7.0.0", 33 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 34 | "@babel/plugin-syntax-import-meta": "^7.0.0", 35 | "@babel/preset-env": "^7.0.0", 36 | "@babel/preset-react": "^7.0.0", 37 | "@babel/register": "^7.0.0", 38 | "babel-eslint": "^8.0.1", 39 | "chai": "^4.1.2", 40 | "eslint": "^4.10.0", 41 | "eslint-plugin-react": "^7.4.0", 42 | "mocha": "^4.0.1", 43 | "nyc": "^11.3.0", 44 | "redux": "^3.7.2", 45 | "redux-mock-store": "^1.3.0", 46 | "redux-promise": "^0.5.3", 47 | "redux-thunk": "^2.2.0", 48 | "sinon": "^4.1.1", 49 | "sinon-chai": "^2.14.0", 50 | "typescript": "^2.6.1", 51 | "typescript-definition-tester": "0.0.5" 52 | }, 53 | "nyc": { 54 | "temp-directory": "coverage/.nyc_output", 55 | "reporter": [ 56 | "html", 57 | "text", 58 | "json" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/src/createDynamicReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { combineReducers } from 'redux' 10 | import concatenateReducers from 'redux-concatenate-reducers' 11 | import filteredReducer from './filteredReducer' 12 | 13 | const expandReducers = reducers => { 14 | const expandedReducers = { children: {} } 15 | 16 | for (let key in reducers) { 17 | let path = key.split('.') 18 | let currentNode = expandedReducers 19 | 20 | for (let element of path) { 21 | if (!currentNode.children[element]) { 22 | currentNode.children[element] = { children: {} } 23 | } 24 | 25 | currentNode = currentNode.children[element] 26 | } 27 | currentNode.reducer = reducers[key] 28 | } 29 | 30 | return expandedReducers 31 | } 32 | 33 | const collapseReducers = node => { 34 | const { reducer, children } = node 35 | 36 | const childrenKeys = Object.keys(children) 37 | 38 | if (!childrenKeys.length) { 39 | return reducer 40 | } 41 | 42 | const reducersToCombine = childrenKeys.reduce( 43 | (reducerMap, key) => ({ ...reducerMap, [key]: collapseReducers(children[key]) }), 44 | {} 45 | ) 46 | 47 | const childrenReducer = combineReducers(reducersToCombine) 48 | 49 | return reducer ? concatenateReducers([filteredReducer(reducer), filteredReducer(childrenReducer)]) : childrenReducer 50 | } 51 | 52 | const createDynamicReducer = reducers => { 53 | const expandedReducers = expandReducers(reducers) 54 | return collapseReducers(expandedReducers) 55 | } 56 | 57 | export default createDynamicReducer 58 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/src/createStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { createStore as baseCreateStore } from 'redux' 10 | import concatenateReducers from 'redux-concatenate-reducers' 11 | import filteredReducer from './filteredReducer' 12 | import createDynamicReducer from './createDynamicReducer' 13 | import flattenReducers from './flattenReducers' 14 | 15 | const DEFAULT_REDUCER = (state) => state 16 | 17 | const createStore = (reducer, ...rest) => { 18 | let dynamicReducers = {} 19 | 20 | const createReducer = () => { 21 | const reducers = [] 22 | 23 | if (reducer) { 24 | reducers.push(filteredReducer(reducer)) 25 | } 26 | 27 | if (Object.keys(dynamicReducers).length !== 0) { 28 | reducers.push(createDynamicReducer(dynamicReducers)) 29 | } 30 | 31 | return Object.keys(reducers).length > 0 ? concatenateReducers(reducers) : DEFAULT_REDUCER 32 | } 33 | 34 | const attachReducers = (reducers) => { 35 | dynamicReducers = { ...dynamicReducers, ...flattenReducers(reducers) } 36 | store.replaceReducer(createReducer()) 37 | } 38 | 39 | const store = baseCreateStore(createReducer(), ...rest) 40 | 41 | store.attachReducers = attachReducers 42 | 43 | return store 44 | } 45 | 46 | export default createStore 47 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/src/filteredReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import isPlainObject from 'lodash.isplainobject' 10 | 11 | const FILTER_INIT = { type: '@@FILTER/INIT' } 12 | 13 | const filteredReducer = (reducer) => { 14 | let knownKeys = Object.keys(reducer(undefined, FILTER_INIT)) 15 | 16 | return (state, action) => { 17 | let filteredState = state 18 | 19 | if (knownKeys.length && state !== undefined) { 20 | filteredState = knownKeys.reduce((current, key) => { 21 | current[key] = state[key]; 22 | return current 23 | }, {}) 24 | } 25 | 26 | let newState = reducer(filteredState, action) 27 | 28 | if (newState === filteredState) { 29 | return state; 30 | } 31 | 32 | if (isPlainObject(newState)) { 33 | knownKeys = Object.keys(newState) 34 | return { 35 | ...state, 36 | ...newState 37 | } 38 | } else { 39 | return newState 40 | } 41 | }; 42 | } 43 | 44 | export default filteredReducer 45 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/src/flattenReducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | const flattenReducers = (reducers, parentKey) => { 10 | if (typeof reducers === 'function') { 11 | return { [parentKey.replace(/\//g, '.')]: reducers } 12 | } 13 | 14 | return Object.keys(reducers).reduce( 15 | (reducerMap, key) => ({ 16 | ...reducerMap, 17 | ...flattenReducers(reducers[key], parentKey ? `${parentKey}.${key}` : key) 18 | }), 19 | {} 20 | ) 21 | } 22 | 23 | export default flattenReducers 24 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { Reducer, Store, StoreEnhancer } from 'redux'; 10 | 11 | export interface StoreCreator { 12 | (): Store; 13 | (reducer: Reducer): Store; 14 | (reducer: Reducer, enhancer: StoreEnhancer): Store; 15 | (reducer: Reducer, preloadedState: S): Store; 16 | (reducer: Reducer, preloadedState: S, enhancer: StoreEnhancer): Store; 17 | } 18 | 19 | export const createStore: StoreCreator; 20 | 21 | declare module "redux" { 22 | 23 | export interface NestableReducersMapObject { 24 | [key: string]: (NestableReducersMapObject | Reducer); 25 | } 26 | 27 | export interface Store { 28 | attachReducers(reducers: NestableReducersMapObject): void; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export { default as createStore } from './createStore' 10 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/createDynamicReducer-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { combineReducers } from 'redux' 10 | import createDynamicReducer from '../src/createDynamicReducer' 11 | 12 | describe('createDynamicReducer tests', () => { 13 | const reducer1 = (state = 'test1', action) => action.value1 || state 14 | const reducer2 = (state = 'test2', action) => action.value2 || state 15 | 16 | it('should combine reducers in single level', () => { 17 | const reducer = createDynamicReducer({ 18 | reducer1, 19 | reducer2 20 | }) 21 | 22 | const initialState = { 23 | reducer1: 'wrong', 24 | reducer2: 'wrong' 25 | } 26 | 27 | const actions = [{ value1: 'expected1' }, { value2: 'expected2' }] 28 | 29 | const state = actions.reduce(reducer, initialState) 30 | 31 | expect(state).to.deep.equal({ 32 | reducer1: 'expected1', 33 | reducer2: 'expected2' 34 | }) 35 | }) 36 | 37 | it('should combine reducers in nested levels', () => { 38 | const reducer = createDynamicReducer({ 39 | 'parent1.reducer1': reducer1, 40 | 'parent1.reducer2': reducer2, 41 | 'parent2.parent3.reducer1': reducer1, 42 | 'parent2.parent3.reducer2': reducer2 43 | }) 44 | 45 | const initialState = { 46 | parent1: { 47 | reducer1: 'wrong', 48 | reducer2: 'wrong' 49 | }, 50 | parent2: { 51 | parent3: { 52 | reducer1: 'wrong', 53 | reducer2: 'wrong' 54 | } 55 | } 56 | } 57 | 58 | const actions = [{ value1: 'expected1' }, { value2: 'expected2' }] 59 | 60 | const state = actions.reduce(reducer, initialState) 61 | 62 | expect(state).to.deep.equal({ 63 | parent1: { 64 | reducer1: 'expected1', 65 | reducer2: 'expected2' 66 | }, 67 | parent2: { 68 | parent3: { 69 | reducer1: 'expected1', 70 | reducer2: 'expected2' 71 | } 72 | } 73 | }) 74 | }) 75 | 76 | it('should combine reducers in different nested levels', () => { 77 | const reducer = createDynamicReducer({ 78 | parent1: combineReducers({ reducer1 }), 79 | 'parent1.reducer2': reducer2, 80 | 'parent2.parent3': combineReducers({ reducer1 }), 81 | 'parent2.parent3.reducer2': reducer2 82 | }) 83 | 84 | const initialState = { 85 | parent1: { 86 | reducer1: 'wrong', 87 | reducer2: 'wrong' 88 | }, 89 | parent2: { 90 | parent3: { 91 | reducer1: 'wrong', 92 | reducer2: 'wrong' 93 | } 94 | } 95 | } 96 | 97 | const actions = [{ value1: 'expected1' }, { value2: 'expected2' }] 98 | 99 | const state = actions.reduce(reducer, initialState) 100 | 101 | expect(state).to.deep.equal({ 102 | parent1: { 103 | reducer1: 'expected1', 104 | reducer2: 'expected2' 105 | }, 106 | parent2: { 107 | parent3: { 108 | reducer1: 'expected1', 109 | reducer2: 'expected2' 110 | } 111 | } 112 | }) 113 | }) 114 | 115 | it('should handle variety of nested levels', () => { 116 | const dummyReducer = (state = { value: 0 }, action) => 117 | action.type === 'INC' ? { ...state, value: state.value + 1 } : state 118 | 119 | const inputStructure = { 120 | foo: dummyReducer, 121 | 'foo.bar': dummyReducer, 122 | 'foo.bar.baz': dummyReducer, 123 | 'foo.bar.qux': dummyReducer, 124 | 'foo.baz': dummyReducer, 125 | bar: dummyReducer, 126 | 'bar.baz': dummyReducer 127 | } 128 | 129 | const reducer = createDynamicReducer(inputStructure) 130 | 131 | const initialState = reducer(undefined, { type: 'INIT' }) 132 | 133 | expect(initialState).to.deep.equal({ 134 | foo: { 135 | value: 0, 136 | bar: { 137 | value: 0, 138 | baz: { 139 | value: 0 140 | }, 141 | qux: { 142 | value: 0 143 | } 144 | }, 145 | baz: { 146 | value: 0 147 | } 148 | }, 149 | bar: { 150 | value: 0, 151 | baz: { 152 | value: 0 153 | } 154 | } 155 | }) 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/createStore-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { combineReducers, applyMiddleware } from 'redux' 10 | import createStore from '../src/createStore' 11 | 12 | describe('createStore Tests', () => { 13 | 14 | const staticReducer1 = (state = 0) => state 15 | const staticReducer2 = (state = "test") => state 16 | 17 | it('should create store with static reducers', () => { 18 | const store = createStore(combineReducers({staticReducer1, staticReducer2})) 19 | const state = store.getState() 20 | 21 | expect(state.staticReducer1).to.equal(0) 22 | expect(state.staticReducer2).to.equal("test") 23 | }) 24 | 25 | it('should create store with no static reducers', () => { 26 | const store = createStore() 27 | const state = store.getState() 28 | 29 | expect(state).to.be.undefined 30 | }) 31 | 32 | it('should create store with initial state', () => { 33 | const store = createStore(combineReducers({staticReducer1}), {staticReducer1: 1}) 34 | const state = store.getState() 35 | 36 | expect(state.staticReducer1).to.equal(1) 37 | }) 38 | 39 | it('should create store with middleware', () => { 40 | let called = false 41 | const middleware = () => next => action => { 42 | called = true 43 | return next(action) 44 | } 45 | 46 | const store = createStore(combineReducers({staticReducer1}), applyMiddleware(middleware)) 47 | 48 | store.dispatch({ type: "TEST" }) 49 | 50 | expect(called).to.be.true 51 | }) 52 | 53 | it('should create store with initial state and middleware', () => { 54 | let called = false 55 | const middleware = () => next => action => { 56 | called = true 57 | return next(action) 58 | } 59 | 60 | const store = createStore(combineReducers({staticReducer1}), {staticReducer1: 1}, applyMiddleware(middleware)) 61 | 62 | store.dispatch({ type: "TEST" }) 63 | const state = store.getState() 64 | 65 | expect(state.staticReducer1).to.equal(1) 66 | expect(called).to.be.true 67 | }) 68 | }); 69 | 70 | describe('attachReducers Tests', () => { 71 | 72 | const staticReducer1 = (state = 0) => state 73 | const dynamicReducer1 = (state = { integer: 0, string: "test"}) => state 74 | const dynamicReducer2 = (state = [ 0, "test" ]) => state 75 | 76 | it('should attach dynamic reducer', () => { 77 | const store = createStore(combineReducers({staticReducer1})) 78 | 79 | store.attachReducers({ dynamicReducer1 }) 80 | 81 | const state = store.getState() 82 | 83 | expect(state.staticReducer1).to.equal(0) 84 | expect(state.dynamicReducer1.integer).to.equal(0) 85 | expect(state.dynamicReducer1.string).to.equal("test") 86 | }) 87 | 88 | it('should attach multiple dynamic reducers', () => { 89 | const store = createStore(combineReducers({staticReducer1})) 90 | 91 | store.attachReducers({ dynamicReducer1, dynamicReducer2 }) 92 | 93 | const state = store.getState() 94 | 95 | expect(state.staticReducer1).to.equal(0) 96 | expect(state.dynamicReducer1.integer).to.equal(0) 97 | expect(state.dynamicReducer1.string).to.equal("test") 98 | expect(state.dynamicReducer2[0]).to.equal(0) 99 | expect(state.dynamicReducer2[1]).to.equal("test") 100 | }) 101 | 102 | it('should attach multiple dynamic reducers at seperate times', () => { 103 | const store = createStore(combineReducers({staticReducer1})) 104 | 105 | store.attachReducers({ dynamicReducer1 }) 106 | store.attachReducers({ dynamicReducer2 }) 107 | 108 | const state = store.getState() 109 | 110 | expect(state.staticReducer1).to.equal(0) 111 | expect(state.dynamicReducer1.integer).to.equal(0) 112 | expect(state.dynamicReducer1.string).to.equal("test") 113 | expect(state.dynamicReducer2[0]).to.equal(0) 114 | expect(state.dynamicReducer2[1]).to.equal("test") 115 | }) 116 | }) -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/filteredReducer-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { combineReducers, createStore } from 'redux' 10 | import filteredReducer from '../src/filteredReducer' 11 | 12 | describe('filteredReducer Tests', () => { 13 | 14 | const primative = (state = 0) => state 15 | const plainObject = (state = { test: "value" }) => state 16 | const array = (state = [ "value" ]) => state 17 | const changingState = (state = {}, action) => action.type == 'ADD_STATE' ? { ...state, test: "value" } : state 18 | 19 | const testCases = [ 20 | { 21 | name: "combined", 22 | reducer: () => combineReducers({ primative, plainObject }), 23 | initialState: { primative: 1, plainObject: { test: "other" } }, 24 | expectedState1: { primative: 0, plainObject: { test: "value" } }, 25 | expectedState2: { primative: 0, plainObject: { test: "value" } }, 26 | expectedState3: { primative: 1, plainObject: { test: "other" } }, 27 | expectedState4: { primative: 1, plainObject: { test: "other" } } 28 | }, 29 | { 30 | name: "primative", 31 | reducer: () => primative, 32 | initialState: 1, 33 | expectedState1: 0, 34 | expectedState2: 0, 35 | expectedState3: 1, 36 | expectedState4: 1 37 | }, 38 | { 39 | name: "plain object", 40 | reducer: () => plainObject, 41 | initialState: { test: "other" }, 42 | expectedState1: { test: "value" }, 43 | expectedState2: { test: "value" }, 44 | expectedState3: { test: "other" }, 45 | expectedState4: { test: "other" } 46 | }, 47 | { 48 | name: "array", 49 | reducer: () => array, 50 | initialState: [ "other" ], 51 | expectedState1: [ "value" ], 52 | expectedState2: [ "value" ], 53 | expectedState3: [ "other" ], 54 | expectedState4: [ "other" ] 55 | }, 56 | { 57 | name: "changing state", 58 | reducer: () => changingState, 59 | initialState: { test: "other" }, 60 | expectedState1: {}, 61 | expectedState2: { test: "value" }, 62 | expectedState3: { test: "other" }, 63 | expectedState4: { test: "value" } 64 | } 65 | ] 66 | 67 | testCases.forEach((testCase) => { 68 | describe(`${testCase.name} reducers`, () => { 69 | 70 | it('should filter', () => { 71 | const store = createStore(filteredReducer(testCase.reducer())) 72 | 73 | expect(store.getState()).to.deep.equal(testCase.expectedState1) 74 | }) 75 | 76 | it('should filter and handle actions', () => { 77 | const store = createStore(filteredReducer(testCase.reducer())) 78 | 79 | store.dispatch({ type: "ADD_STATE" }) 80 | 81 | expect(store.getState()).to.deep.equal(testCase.expectedState2) 82 | }) 83 | 84 | it('should filter with initial state', () => { 85 | const store = createStore(filteredReducer(testCase.reducer()), testCase.initialState) 86 | 87 | expect(store.getState()).to.deep.equal(testCase.expectedState3) 88 | }) 89 | 90 | it('should filter with initial state and handle actions', () => { 91 | const store = createStore(filteredReducer(testCase.reducer()), testCase.initialState) 92 | 93 | store.dispatch({ type: "ADD_STATE" }) 94 | 95 | expect(store.getState()).to.deep.equal(testCase.expectedState4) 96 | }) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/flattenReducers-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import flattenReducers from '../src/flattenReducers' 10 | 11 | describe('flattenReducers Tests', () => { 12 | const reducer = () => null 13 | 14 | it('should not change basic reducer structure', () => { 15 | const reducers = { 16 | parent1: reducer, 17 | parent2: reducer 18 | } 19 | 20 | const flattenedReducers = flattenReducers(reducers) 21 | 22 | expect(flattenedReducers).to.deep.equal({ 23 | parent1: reducer, 24 | parent2: reducer 25 | }) 26 | }) 27 | 28 | it('should flatten reducer structure', () => { 29 | const reducers = { 30 | parent1: { 31 | parent2: reducer 32 | } 33 | } 34 | 35 | const flattenedReducers = flattenReducers(reducers) 36 | 37 | expect(flattenedReducers).to.deep.equal({ 38 | 'parent1.parent2': reducer 39 | }) 40 | }) 41 | 42 | it('should flatten deeply nested reducer structure', () => { 43 | const reducers = { 44 | parent1: { 45 | parent2: { 46 | parent3: reducer 47 | } 48 | } 49 | } 50 | 51 | const flattenedReducers = flattenReducers(reducers) 52 | 53 | expect(flattenedReducers).to.deep.equal({ 54 | 'parent1.parent2.parent3': reducer 55 | }) 56 | }) 57 | 58 | it('should flatten differently nested reducer structure', () => { 59 | const reducers = { 60 | parent1: { 61 | parent2: reducer, 62 | parent3: reducer 63 | }, 64 | parent3: { 65 | parent4: reducer, 66 | parent5: { 67 | parent6: reducer 68 | } 69 | }, 70 | parent7: reducer 71 | } 72 | 73 | const flattenedReducers = flattenReducers(reducers) 74 | 75 | expect(flattenedReducers).to.deep.equal({ 76 | 'parent1.parent2': reducer, 77 | 'parent1.parent3': reducer, 78 | 'parent3.parent4': reducer, 79 | 'parent3.parent5.parent6': reducer, 80 | parent7: reducer 81 | }) 82 | }) 83 | 84 | it('should normalize nested path seperator', () => { 85 | const reducers = { 86 | 'parent1/parent2': reducer, 87 | 'parent1/parent2/parent3': reducer 88 | } 89 | 90 | const flattenedReducers = flattenReducers(reducers) 91 | 92 | expect(flattenedReducers).to.deep.equal({ 93 | 'parent1.parent2': reducer, 94 | 'parent1.parent2.parent3': reducer 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import babelRegister from 'babel-register' 10 | 11 | babelRegister() 12 | 13 | global.expect = require('chai').expect 14 | global.sinon = require('sinon') 15 | 16 | var chai = require("chai") 17 | var sinonChai = require("sinon-chai") 18 | global.expect = chai.expect 19 | global.assert = chai.assert 20 | chai.use(sinonChai) 21 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/typescript/definitions-spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import * as ts from 'typescript' 10 | import * as tt from 'typescript-definition-tester' 11 | import path from 'path' 12 | import fs from 'fs' 13 | 14 | describe('TypeScript definitions', function () { 15 | 16 | const options = { 17 | noEmitOnError: true, 18 | noImplicitAny: true, 19 | target: ts.ScriptTarget.ES5, 20 | module: ts.ModuleKind.CommonJS, 21 | jsx: ts.JsxEmit.React 22 | } 23 | 24 | fs.readdirSync(path.join(__dirname, 'definitions')).forEach((filename) => { 25 | it(`should compile ${path.basename(filename, path.extname(filename))} against index.d.ts`, (done) => { 26 | tt.compile([path.join(__dirname, 'definitions', filename)], options, done) 27 | }).timeout(20000) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/typescript/definitions/attachReducers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { combineReducers, Reducer, Action } from 'redux' 10 | import { createStore } from '../../../src' 11 | 12 | class TestState { 13 | value: number 14 | } 15 | 16 | const testReducer: Reducer = (state: TestState = { value: 0 }, action: Action) => { 17 | return state 18 | } 19 | 20 | const store = createStore(combineReducers({ testReducer1: testReducer })) 21 | 22 | store.attachReducers({ testReducer2: testReducer }) 23 | 24 | store.attachReducers({ testReducer3: testReducer, testReducer4: testReducer }) 25 | 26 | store.attachReducers({ nested: { testReducer5: testReducer } }) 27 | store.attachReducers({ 'nested.testReducer6': testReducer }) 28 | store.attachReducers({ 'nested/testReducer7': testReducer }) 29 | 30 | 31 | store.attachReducers({ 32 | testReducer8: testReducer, 33 | nested: { 34 | testReducer9: testReducer, 35 | testReducer10: testReducer 36 | }, 37 | 'nested.testReducer11': testReducer, 38 | 'nested/testReducer12': testReducer 39 | }) 40 | -------------------------------------------------------------------------------- /packages/redux-dynamic-reducer/test/typescript/definitions/createStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, IOOF Holdings Limited. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import { combineReducers, Reducer, Dispatch, Action, applyMiddleware, Middleware, MiddlewareAPI } from 'redux' 10 | import { createStore } from '../../../src' 11 | 12 | class TestState { 13 | value: number 14 | } 15 | 16 | const testReducer: Reducer = (state: TestState = { value: 0 }, action: Action) => { 17 | return state 18 | } 19 | 20 | const testEnhancer: Middleware = ({ dispatch, getState }) => (next) => (action) => next(action); 21 | 22 | const store1 = createStore() 23 | 24 | const store2 = createStore(combineReducers({ testReducer })) 25 | 26 | const store3 = createStore(combineReducers({ testReducer }), { value: 1}) 27 | 28 | const store4 = createStore(combineReducers({ testReducer }), applyMiddleware(testEnhancer)) 29 | 30 | const store5 = createStore(combineReducers({ testReducer }), { value: 1}, applyMiddleware(testEnhancer)) 31 | --------------------------------------------------------------------------------