|
| 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 | +Use this library to attach additional reducer functions to an existing [Redux](http://redux.js.org/) store at runtime. |
| 8 | + |
| 9 | +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). |
| 10 | + |
| 11 | +## Why this library? |
| 12 | + |
| 13 | +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. |
| 14 | + |
| 15 | +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. |
| 16 | + |
| 17 | +## The common use case |
| 18 | + |
| 19 | +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. |
| 20 | + |
| 21 | +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. |
| 22 | + |
| 23 | +## Packages |
| 24 | + |
| 25 | +* [`redux-dynamic-reducer`](/packages/redux-dynamic-reducer): The core package for `redux-dynamic-reducer` |
| 26 | +* [`react-redux-dynamic-reducer`](/packages/redux-dynamic-reducer): React bindings for `redux-dynamic-reducer` |
| 27 | + |
| 28 | +## How to use |
| 29 | + |
| 30 | +### 1. Create the store |
| 31 | + |
| 32 | +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: |
| 33 | + |
| 34 | +```javascript |
| 35 | +import { combineReducers } from 'redux' |
| 36 | +import { createStore } from 'redux-dynamic-reducer' |
| 37 | + |
| 38 | +... |
| 39 | + |
| 40 | +const reducer = combineReducers({ staticReducer1, staticReducer2 }) |
| 41 | +const store = createStore(reducer) |
| 42 | +``` |
| 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. Dynamically attach a reducer |
| 57 | + |
| 58 | +#### Not using redux-subspace |
| 59 | + |
| 60 | +Call `attachReducers` on the store with your dynamic reducers to attach them to the store at runtime: |
| 61 | + |
| 62 | +```javascript |
| 63 | +store.attachReducers({ dynamicReducer }) |
| 64 | +``` |
| 65 | + |
| 66 | +Multiple reducers can be attached as well: |
| 67 | + |
| 68 | +```javascript |
| 69 | +store.attachReducers({ dynamicReducer1, dynamicReducer2 }) |
| 70 | +``` |
| 71 | + |
| 72 | +#### When using React and redux-subspace |
| 73 | + |
| 74 | +First, wrap the component with `withReducer`: |
| 75 | + |
| 76 | +```javascript |
| 77 | +// in child component |
| 78 | +import { withReducer } from 'react-redux-dynamic-reducer |
| 79 | +
|
| 80 | +export default withReducer(myReducer, 'defaultKey')(MyComponent) |
| 81 | +``` |
| 82 | +
|
| 83 | +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. |
| 84 | +
|
| 85 | +Mount your component somewhere inside a react-redux `Provider`: |
| 86 | +
|
| 87 | +```javascript |
| 88 | +// in parent app/component |
| 89 | +import MyComponent from './MyComponent' |
| 90 | +
|
| 91 | +<Provider store={store}> |
| 92 | + ... |
| 93 | + <MyComponent /> |
| 94 | + ... |
| 95 | +</Provider> |
| 96 | +``` |
| 97 | +
|
| 98 | +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. |
| 99 | +
|
| 100 | +Multiple instances of the same component can be added by overriding the default subspace key with an instance specific key: |
| 101 | +
|
| 102 | +```javascript |
| 103 | +// in parent app/component |
| 104 | +import MyComponent from './MyComponent' |
| 105 | +
|
| 106 | +... |
| 107 | +
|
| 108 | +const MyComponent1 = MyComponent.createInstance('myInstance1') |
| 109 | +const MyComponent2 = MyComponent.createInstance('myInstance2') |
| 110 | +
|
| 111 | +... |
| 112 | +
|
| 113 | +<Provider store={store}> |
| 114 | + <MyComponent1 /> |
| 115 | + <MyComponent2 /> |
| 116 | +</Provider> |
| 117 | +``` |
| 118 | +
|
| 119 | +Additional state can be mapped for the component or an instance of the component by providing an additional mapper: |
| 120 | +
|
| 121 | +```javascript |
| 122 | +export default withReducer(myReducer, 'defaultKey', { mapExtraState: (state, rootState) => ({ /* ... */ }) })(MyComponent) |
| 123 | +
|
| 124 | +... |
| 125 | +
|
| 126 | +const MyComponentInstance = MyComponent |
| 127 | + .createInstance('instance') |
| 128 | + .withExtraState((state, rootState) => ({ /* ... */ }) }) |
| 129 | +
|
| 130 | +... |
| 131 | +
|
| 132 | +const MyComponentInstance = MyComponent |
| 133 | + .createInstance('instance') |
| 134 | + .withOptions({ mapExtraState: (state, rootState) => ({ /* ... */ }) }) |
| 135 | +``` |
| 136 | +
|
| 137 | +The extra state is merged with the bundled reducer's state. |
| 138 | + |
| 139 | +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: |
| 140 | + |
| 141 | +```javascript |
| 142 | +export default withReducer(myReducer, 'defaultKey', { namespaceActions: false })(MyComponent) |
| 143 | +
|
| 144 | +... |
| 145 | +
|
| 146 | +const MyComponentInstance = MyComponent.createInstance('instance').withOptions({ namespaceActions: false }) |
| 147 | +``` |
| 148 | + |
| 149 | +## Examples |
| 150 | + |
| 151 | +Examples can be found [here](/examples). |
| 152 | + |
| 153 | +## Limitations |
| 154 | + |
| 155 | +* Each dynamic reducer needs a unique key |
| 156 | + * If the same key is used, the last component to use it wins |
| 157 | +* Currently, reducers are only ever attached at the root of the store. Nesting is a complex problem we are working on. Components can nested as deep as required, but the store state tree will not match the nesting structure. Consequently, the subspace keys must be unique across the board. |
0 commit comments