npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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 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>
}