@zumper/redux-add-reducer
v0.1.2
Published
Redux store enhancer for adding reducers
Downloads
6
Readme
@zumper/redux-add-reducer
Redux store enhancer that makes an addReducer
method available. The technique is borrowed from the now deprecated react-redux-starter-kit
.
Why?
The enhanced store provides a new store.addReducer(key, reducer)
method that makes it practical to code-split redux modules.
The key technology here it to leverage the little-known store.replaceReducer
method to enable "injecting" a new reducer.
The principle use case is for code-splitting modules. If you have a redux module that would only be used for a given route, it would be beneficial to code-split your module. In a normal redux app this would not be possible.
Install
yarn add @zumper/redux-add-reducer
Setup
Redux is somewhat unopinionated on how you structure your reducers. However, it makes a combineReducers
function available for merging several reducers together. The addReducer
method added by this store enhancer is designed to work with combineReducers
.
It is expected that you maintain a reducerMap
, which will be used to create the initial rootReducer
. When adding a reducer, first the key
is added to the reducerMap
and a then new rootReducer
is created using combineReducers
.
The reducerMap
must be compatible with combineReducers
.
with store enhancer
This example shows how to use the provided store enhancer to add the extra methods to the redux store.
import thunk from 'redux-thunk'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import { addReducerEnhancer } from '@zumper/redux-add-reducer'
import { reducerMap } from 'modules'
export const createAppStore = (preloadedState) => {
const middleware = [thunk]
const enhancer = compose(
applyMiddleware(...middleware),
addReducerEnhancer(reducerMap)
)
const rootReducer = combineReducers(reducerMap)
return createStore(rootReducer, preloadedState, enhancer)
}
without the store enhancer
Store enhancers have largely fallen out of favor. If you'd like to skip the confusing pageantry of composing your store enhancers, you can directly mutate the store.
import thunk from 'redux-thunk'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import { mutateStore } from '@zumper/redux-add-reducer'
import { reducerMap } from 'modules'
export const createAppStore = (preloadedState) => {
const middleware = [thunk]
const enhancer = applyMiddleware(...middleware)
const rootReducer = combineReducers(reducerMap)
const store = createStore(rootReducer, preloadedState, enhancer)
// manually "enhance" the store
return mutateStore(store, reducerMap)
}
without mutateStore
Alternative, directly mutating.
import thunk from 'redux-thunk'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import {
createAddReducer,
createRemoveReducer,
} from '@zumper/redux-add-reducer'
import { reducerMap } from 'modules'
export const createAppStore = (preloadedState) => {
const middleware = [thunk]
const enhancer = applyMiddleware(...middleware)
const rootReducer = combineReducers(reducerMap)
const store = createStore(rootReducer, preloadedState, enhancer)
// manually "enhance" the store
store.reducerMap = reducerMap
store.addReducer = createAddReducer(store)
store.removeReducer = createRemoveReducer(store)
return store
}
API
addReducerEnhancer(reducerMap)
Store enhancer that adds reducerMap
, addReducer
, removeReducer
to the store interface. Requires a reducerMap
, a plain javascript object, compatible with combineReducers
.
import { addReducerEnhancer } from '@zumper/redux-add-reducer'
const reducerMap = {
app: appReducer,
homepageView: homepageViewReducer,
}
const middleware = [thunk]
const enhancer = compose(
applyMiddleware(...middleware),
addReducerEnhancer(reducerMap)
)
const reducer = combineReducers(reducerMap)
const store = createStore(reducer, preloadedState, enhancer)
mutateStore(store, reducerMap)
User internally by addReducerEnhancer
to add reducerMap
, addReducer
, removeReducer
to the store interface.
import { mutateStore } from '@zumper/redux-add-reducer'
const reducerMap = {
app: appReducer,
homepageView: homepageViewReducer,
}
const middleware = [thunk]
const enhancer = compose(applyMiddleware(...middleware))
const reducer = combineReducers(reducerMap)
const store = mutateStore(
createStore(reducer, preloadedState, enhancer),
reducerMap
)
createAddReducer(store)
and createRemoveReducer(store)
Binds the addReducer
and removeReducer
methods to the store.
import {
createAddReducer,
createRemoveReducer,
} from '@zumper/redux-add-reducer'
const reducerMap = {
app: appReducer,
homepageView: homepageViewReducer,
}
const middleware = [thunk]
const enhancer = compose(applyMiddleware(...middleware))
const reducer = combineReducers(reducerMap)
const store = createStore(reducer, preloadedState, enhancer)
store.reducerMap = reducerMap
store.addReducer = createAddReducer(store)
store.removeReducer = createRemoveReducer(store)
store.reducerMap
A plain javascript object, compatible with combineReducers
. Used to create a new rootReducer
. The reducer map is used by addReducer
and removeReducer
to manage the top-level reducers.
store.addReducer(key, reducer)
Manipulates store.reducerMap
to add a new reducer at the given key. It feeds the new reducerMap
into combineReducers
and uses store.replacerReducer
to replace the rootReducer
on the store.
store.removeReducer(key)
Manipulates store.reducerMap
to delete the given key. It feeds the new reducerMap
into combineReducers
and uses store.replacerReducer
to replace the rootReducer
on the store.
useReduxReducer(key, reducer, options)
React hook for adding a reducer from within a component.
key
- a string, becomes a top-level key in your redux state.reducer
- a reducer functionoptions
- an objectshouldRemoveOnCleanup
, which will callremoveReducer
during the cleanup phase for the hook. Defaults tofalse
.
import { useReduxReducer } from '@zumper/redux-add-reducer'
const key = 'myView'
const reducer = (state, action) => null
const MyView = () => {
useReduxReducer(key, reducer)
return <div>Hello</div>
}
withReduxReducer(key, reducer, options)
HOC for adding a reducer from within a component. Thin wrapper around the hook. Takes the exact same arguments. More practical if you are using react-redux connect
.
key
- a string, becomes a top-level key in your redux state.reducer
- a reducer functionoptions
- an objectshouldRemoveOnCleanup
, which will callremoveReducer
during the cleanup phase for the hook. Defaults tofalse
.
import { withReduxReducer } from '@zumper/redux-add-reducer'
const key = 'myView'
const reducer = (state, action) => null
const MyView = () => {
return <div>Hello</div>
}
const mapStateToProps = (state) => {
return {}
}
const MyViewContainer = compose(
withReduxReducer(key, reducer),
connect(mapStateToProps)
)(MyView)
Usage
Once your store is enhanced you'll be able to add a reducer to the redux store so long as you have access to the enhanced store
instance.
Below you can see an example component that is the main view of a lazy-loaded route.
import loadable from '@loadable/components'
// used in a react-router route
const MyLazyLoadedView = loadable(() => import('./MyView'))
The view itself would add the reducer for the route.
import React from 'react'
// get a reducer for the route's module
import reducer from 'modules/myModule'
import { useReduxReducer } from '@zumper/redux-add-reducer'
// by convention the key should match the module name
const key = 'myModule'
const MyView = () => {
// add a reducer
useReduxReducer(key, reducer)
// render the view normally
return <div>My View</div>
}
Using withReduxReducer
with withLoadData
If you are wrapping a route with a withLoadData
HOC you will need to to take care to add the reducer before trying to load the data. You can use the provided withReduxReducer
HOC to make it easier to compose your HOCs together.
import { withLoadData } from '@zumper/load-data' // <-- imaginary package
import { withReduxReducer } from '@zumper/redux-add-reducer'
import reducer from 'modules/myModule'
import loadData from './loadData'
import MyView from './MyView'
const key = 'myModule'
const MyViewContainer = compose(
withReduxReducer(key, reducer) // <-- must be "before" load data
withLoadData(loadData),
connect(mapStateToProps, mapDispatchToProps),
)(MyView)
export default MyViewContainer
Advanced usage: replacing deep reducers
The reducerMap
must be a shallow object to work with combineReducers
. This means that you can only add top-level reducers. It would be conceptually possible to add a reducer deep into your state tree but this is not directly supported.
import routesReducer from 'modules/routes'
// example state shape
const state = {
app,
routes: {
shoes,
// <-- we want to add "pants" here
},
}
// normally the reducerMap is "shallow"
const reducerMap = {
app: appReducer,
routes: routesReducer, // <-- it's not "easy" to add a sub-key reducer
}
We can get around the shallow object limitation by creating a reducer map for our routes reducer. By mutating this map we can recreate the routesReducer
with the pantsReducer
at the pants
key.
First we need to create a special function for adding sub reducers. We can handle this in a generic way as long as the parent reducer makes a reducerMap
available.
// you can create your own special method
export const createAddSubReducer = (store, parentKey, parentReducerMap) => (
key,
reducer
) => {
if (Object.hasOwnProperty.call(parentReducerMap, key)) {
return
}
parentReducerMap[key] = reducer
const parentReducer = combineReducers(parentReducerMap)
delete store.reducerMap[parentKey]
store.addReducer(parentKey, parentReducer)
}
Next, we can enhance the store to make store.addRouteReducer
available. Here we can see the store setup example from above. Notice that we're extending the store.
import thunk from 'redux-thunk'
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
import { addReducerEnhancer } from '@zumper/redux-add-reducer'
import { reducerMap } from 'modules'
import {
reducer as routesReducer,
reducerMap as routesReducerMap,
} from 'modules/routes'
import { createAddSubReducer } from './createAddSubReducer'
export const createAppStore = (preloadedState) => {
const middleware = [thunk]
const enhancer = compose(
applyMiddleware(...middleware),
addReducerEnhancer(reducerMap)
)
const rootReducer = combineReducers(reducerMap)
const store = createStore(rootReducer, preloadedState, enhancer)
// you can extend the enhanced store
store.addRouteReducer = createAddSubReducer(store, 'routes', routesReducerMap)
return store
}
Finally, we can add new sub-reducers to the routesReducer
from within our app.
import React, { useEffect } from 'react'
import { useStore } from 'react-redux'
import { reducer } from 'modules/pants'
const key = 'pants'
const MyView = () => {
// access the store
const store = useStore()
// add the sub reducer on render
store.addRouteReducer(key, reducer)
// render the view normally
return <div>hello</div>
}