@zumper/redux-add-reducer

0.1.2 • Public • Published

@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 function
  • options - an object
    • shouldRemoveOnCleanup, which will call removeReducer during the cleanup phase for the hook. Defaults to false.
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 function
  • options - an object
    • shouldRemoveOnCleanup, which will call removeReducer during the cleanup phase for the hook. Defaults to false.
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>
}

Package Sidebar

Install

npm i @zumper/redux-add-reducer

Weekly Downloads

1

Version

0.1.2

License

MIT

Unpacked Size

57.7 kB

Total Files

23

Last publish

Collaborators

  • dloehr