Dynamic Redux Reducers

This post is specific to a need I had on recent React / Redux project. It’s a common need and one I’d run into before, but this was the first time I needed to come up with a solution for it. This was difficult for me. I had to slow down and take time to internalize what I was trying to do and all the pieces involved. My hope is that this post will help someone else also working to figure this out.

I’ll detail my process in this post. Here’s a live demo and an editable sandbox. The best way to see the effects is to use the Redux DevTools Chrome extension.

If you’re reading this I’m going to assume you have knowledge of Redux and are using it with React by way of react-redux. I'm also going to assume you’re looking for a solution to a similar problem.

What am I trying to do and why?

In standard Redux usage, you provide reducer functions at the time you create the store with createStore. I wanted a way to add reducer functions later, on demand.

A lot of folks need this because their reducers are not available at createStore time due to code-splitting. That’s a perfect use for dynamic reducers.

My project doesn’t use code-splitting. For this, dynamic reducers were a preference. I didn’t want to spread info about modules throughout the project structure. I wanted each feature to live in a directory, isolated as much as possible. That meant co-locating reducers, components, styles, and so on. I could do that and still import the reducers to the main reducer creation, but that would couple module reducers to the main reducer.

Existing solutions

In my Googling for an existing solution I landing on this Stack Overflow question and answer. The answer is from Dan Abramov so I knew it was a way to go. My solution uses most of the code from that answer.

In Dan’s answer, it all made sense to me until his example of how to inject reducers. I’m using React Router, but I don’t define routes the way he described. I didn’t want to have to change how I defined my routes for this. I also couldn’t find official documentation for methods he used in his example so I wanted to avoid copy / paste. I also wanted to fully understand the code I was adding to my project.

It’s worth mentioning two projects I came across in my search. redux-dynamic-reducer and paradux. I didn’t try either of them because I didn’t see the need in adding another dependency, but they might work for you.

What the demo shows

The demo shows a simple page with a link to /records. When page loads, the Redux state tree contains two keys. One for each reducer function introduced at store creation.

There’s a link to the /records page. When you navigate to that page, I add another reducer function for Records. In the rest of this post I’ll decribe how I do that.

The code

You can follow along in the CodeSandbox. I’ll start with creating the root reducer in /rootReducer.js.

import { combineReducers } from "redux";
import layout from "./reducers/layout";
import home from "./reducers/home";

/**
 * @param {Object} - key/value of reducer functions
 */
const createReducer = asyncReducers =>
  combineReducers({
    home,
    layout,
    ...asyncReducers
  });

export default createReducer;

I pulled this code from Dan’s SO answer. It has two reducer functions; layout and home. They’re global reducers, not module level, so they fit well in the root reducer.

The key detail here is the asyncReducers parameter. Adding the contents of it to the object given to combineReducers is how we add reducers later.

Next up is store creation in /initializeStore.js. Again, most of this code is from Dan’s example.

import { createStore } from "redux";
import createReducer from "./rootReducer";

const initializeStore = () => {
  const store = createStore(createReducer());

  store.asyncReducers = {};
  store.injectReducer = (key, reducer) => {
    store.asyncReducers[key] = reducer;
    store.replaceReducer(createReducer(store.asyncReducers));
    return store;
  };

  return store;
};

export default initializeStore;

The first line of initializeStore is where we create the Redux store with the initial reducers from createReducer. In standard Redux usage, this is all you’d need. The store is set up and ready with the home and layout reducers.

createStore returns a plain object, so we’ll take advantage of that by tacking helpful items onto it. We’ll use store.asyncReducers to house our dynamic reducers. With store.injectReducer I deviate from Dan’s example. The function does the same thing as his injectAsyncReducer, but I attach it to the store object for convenience that I’ll show later.

injectReducer has two responsibilities. First, store all dynamic reducers in asyncReducers. This ensures that each time we invoke injectReducer we don’t lose other dynamic reducers. Next up is the main work. replaceReducer isn’t custom, it’s part of Redux. It does does what it says on the tin. Invoking it replaces the reducer function with one you give it.

Where things got tricky for me

At this point everything seemed straightforward to me, but then I got lost fast. I have a store, I have a function to add new reducers. But where can I access that function to invoke it? In all my frantic Googling, I couldn’t find an example that worked for my setup. So, I sat down to figure out a solution.

It took me a while to figure out where I could access that store object. I had clues though. In my entry point file /index.js I use the Provider component. This is standard for React / Redux projects.

import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import initializeStore from "./initializeStore";
import App from "./App";

const store = initializeStore();
render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Giving the store to Provider makes it available to all child components by way of the connect function. I read more about it and learned that store is also available in the context of each component. If you’ve read anything about React context, you’ve read that you probably shouldn’t use it. For my purposes here it seemed isolated enough to be OK. Time will tell if that’s correct or not. More details on my context usage to later.

Putting the pieces together

I want to use as little code as possible to add reducers. I do that with a higher-order component in /withReducer.js.

import React from "react";
import { object } from "prop-types";

const withReducer = (key, reducer) => WrappedComponent => {
  const Extended = (props, context) => {
    context.store.injectReducer(key, reducer);
    return <WrappedComponent {...props} />
  };

  Extended.contextTypes = {
    store: object
  };

  return Extended;
};

export { withReducer };

And example usage in routes/Records/Records.js:

import { withReducer } from "../../withReducer";
import reducer from "./ducks";

const Records = () => (...);
export default withReducer("records", reducer)(Records);

I’ll start with usage in Records.js. I import the records reducer from routes/Records/ducks/index.js. The reducer doesn’t do much. It sets hard-coded initial state, then returns it as-is. The component acts like a container component. I could connect it, but for the purposes of this demo, left it out.

The pertinent bit is the last line. There I invoke withReducer and provide it a key of “records” and the record reducer. Then I invoke the returned function, providing the Records component.

Records is a React component I import to use as the value of the component property of a React Router <Route />.

The withReducer component

withReducer is a Higher-Order Component. The key parameter becomes the key in the Redux state tree. The reducer parameter is the reducer to add. It returns a function that accepts a single parameter, WrappedComponent. That’s expected to be a valid React component. In the earlier usage example, that’s the Records component.

I’ll jump ahead to an important part of withReducer that was new to me and might be confusing.

...
Extended.contextTypes = {
  store: object
};
...

Extended is a stateless component, so it must define a contextTypes property to gain access to context. From the React docs:

Stateless functional components are also able to reference context if contextTypes is defined as a property of the function.

reactjs.org/docs/context.html#referencing-context-in-stateless-functional-components

In contextTypes I defined the property I want to access in the component, store. That uses the object type from the prop-types library.

When a component defines a contextTypes property, it receives a second parameter, context. That’s visible in the Extended signature:

...
const Extended = (props, context) => {...}
...

Extended now has access to the store object. That’s because <Provider store={store}> in /index.js makes it available to all child components via context.

This happens in the Provider.js source with getChildContext and childContextTypes. That code is good reading if you’re looking for examples of context usage.

In initializeStore.js I created a function on the store object, store.injectReducer. Now, I use that to add the new reducer:

...
const Extended = (props, context) => {
  context.store.injectReducer(key, reducer);
  return <WrappedComponent {...props} />;
};
...

The orginal component doesn’t change. Extended only returns it with any original properties.

How to see this working

At this point, the code works. But, this type of change can be difficult to visualize. As mentioned earlier, the Redux DevTools Chrome extension. works best for me. In the demo I included the devtools snippet when creating the store. If you install the extension and view the Redux panel, you can see new reducers change the state tree.

Animated gif showing a new reducer added in Redux devtools Chrome extension.
Demo of the records reducer being added when navigating to the /records route.

To further show the results in the demo, I connected the record route to display record data from the store.

...
const mapStateToProps = (state, props) => {
  const { match: { params: { id } } } = props;

  return {
    recordId: id,
    record: state.records[id] || {}
  };
};

export default connect(mapStateToProps)(Record);

The full code is in /routes/Records/routes/Record.js.

A solution

As I mentioned earlier, this is a common need in React/Redux projects for different reasons. I’ve used other, similar methods for dynamic routes in the past. Other folks have different approaches. With that, this is a solution, not necessiarly the solution.

If this is helpful and you use it as-is or change it to fit your needs, let me know. There’s always room for improvement.

Thanks for reading