├── .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 | [](https://www.npmjs.com/package/redux-dynamic-reducer)
8 | [](https://www.npmjs.com/package/redux-dynamic-reducer)
9 | [](/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 |
28 | +
29 |
30 | {' '}
31 |
32 | -
33 |
34 | {' '}
35 |
36 | Increment if odd
37 |
38 | {' '}
39 |
40 | Increment async
41 |
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 |
40 | Dismiss
41 |
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 |
49 | Go!
50 |
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 |
28 | {isFetching ? 'Loading...' : 'Load More'}
29 |
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 |
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 |
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 |
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 | [](https://www.npmjs.com/package/react-redux-dynamic-reducer)
4 | [](https://www.npmjs.com/package/react-redux-dynamic-reducer)
5 | [](/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 | [](https://www.npmjs.com/package/redux-dynamic-reducer)
4 | [](https://www.npmjs.com/package/redux-dynamic-reducer)
5 | [](/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 |
--------------------------------------------------------------------------------