Dynamic Redux Reducers
Originally published on my personal blog https://tylergaw.com/articles/dynamic-redux-reducers
This post is specific to a need I had on a 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 the 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 with Redux 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 good 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 the 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. I’ll show that 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 what it says on the tin: invoking it replaces the reducer function with the 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 later.
Putting the pieces together
I wanted to use as little code as possible to add reducers. I did 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 };
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 a 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
, which is 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 well for this purpose. 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.
To further show the results in the demo, I connect
ed 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 said, this is a solution, not necessarily 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
Originally published at tylergaw.com.