193

I'm going migrate to Redux.

My application consists of a lot of parts (pages, components) so I want to create many reducers. Redux examples show that I should use combineReducers() to generate one reducer.

Also as I understand Redux application should have one store and it is created once the application starts. When the store is being created I should pass my combined reducer. This makes sense if the application is not too big.

But what if I build more than one JavaScript bundle? For example, each page of application has own bundle. I think in this case the one combined reducer is not good. I looked through the sources of Redux and I have found replaceReducer() function. It seems to be what I want.

I could create combined reducer for each part my application and use replaceReducer() when I move between parts of application.

Is this a good approach?

Dan Abramov
  • 241,321
  • 75
  • 389
  • 492
Pavel Teterin
  • 1,961
  • 3
  • 11
  • 9

6 Answers6

255

Update: see also how Twitter does it.

This is not a full answer but should help you get started. Note that I'm not throwing away old reducers—I'm just adding new ones to the combination list. I see no reason to throw away the old reducers—even in the largest app you're unlikely to have thousands of dynamic modules, which is the point where you might want to disconnect some reducers in your application.

reducers.js

import { combineReducers } from 'redux';
import users from './reducers/users';
import posts from './reducers/posts';

export default function createReducer(asyncReducers) {
  return combineReducers({
    users,
    posts,
    ...asyncReducers
  });
}

store.js

import { createStore } from 'redux';
import createReducer from './reducers';

export default function configureStore(initialState) {
  const store = createStore(createReducer(), initialState);
  store.asyncReducers = {};
  return store;
}

export function injectAsyncReducer(store, name, asyncReducer) {
  store.asyncReducers[name] = asyncReducer;
  store.replaceReducer(createReducer(store.asyncReducers));
}

routes.js

import { injectAsyncReducer } from './store';

// Assuming React Router here but the principle is the same
// regardless of the library: make sure store is available
// when you want to require.ensure() your reducer so you can call
// injectAsyncReducer(store, name, reducer).

function createRoutes(store) {
  // ...

  const CommentsRoute = {
    // ...

    getComponents(location, callback) {
      require.ensure([
        './pages/Comments',
        './reducers/comments'
      ], function (require) {
        const Comments = require('./pages/Comments').default;
        const commentsReducer = require('./reducers/comments').default;

        injectAsyncReducer(store, 'comments', commentsReducer);
        callback(null, Comments);
      })
    }
  };

  // ...
}

There may be neater way of expressing this—I'm just showing the idea.

Dan Abramov
  • 241,321
  • 75
  • 389
  • 492
  • 13
    I would love to see this type of functionality added to the project. The ability to add reducers dynamically is a must when dealing with code splitting and large applications. I have entire sub trees that may not be accessed by some users and loading all the reducers is a waste. Even with redux-ignore large applications can really stack up reducers. – JeffBaumgardt Apr 27 '16 at 14:07
  • 2
    Sometimes, it's a bigger waste to 'optimize' something inconsequential. – XML May 18 '16 at 05:10
  • I discovered a problem or limitation with this approach. Say you want to have a branch called user on your state tree, and it has some state that gets loaded from two different sets of actions and reducers. The first set has actions that deals with the user's permissions, and has corresponding reducers to update the state when these actions get fired. The other set of actions and reducers deal with personal data like address, city, etc. If we load the first set of reducers when the homepage gets hit, and the second one when they log into their profile, then it will not combine these reducers. – BryceHayden Jun 09 '16 at 05:06
  • 1
    Hopefully the above comment makes sense...as I ran out of room. But basically I don't see an easy way to have the reducers combined when into a single branch on our state tree when they are dynamically loaded from different routes ```/homepage``` and then more of that branch gets loaded when the user goes to their ```profile.``` An example of how to do this, would be awesome. Otherwise I have a hard time flattening out my state tree or I have to have very specific branch names ```user-permissions``` and ```user-personal``` – BryceHayden Jun 09 '16 at 05:10
  • 1
    And how should I act, if I have initial state? – Stalso Jun 10 '16 at 13:14
  • 3
    https://github.com/mxstbr/react-boilerplate boilerplate uses the exact same technique mentioned here to load reducers. – Pouya Sanooei Aug 15 '16 at 12:55
  • Great answer. Would a similar solution work with a TypeScript implementation? – Jan P Nov 27 '17 at 12:57
  • Now that react-router has moved to v4 and does not support automagically calling getComponent inside the route definition, how do you go about injecting reducers? – tommyalvarez Mar 28 '18 at 16:14
26

This is how I implemented it in a current app (based on code by Dan from a GitHub issue!)

// Based on https://github.com/rackt/redux/issues/37#issue-85098222
class ReducerRegistry {
  constructor(initialReducers = {}) {
    this._reducers = {...initialReducers}
    this._emitChange = null
  }
  register(newReducers) {
    this._reducers = {...this._reducers, ...newReducers}
    if (this._emitChange != null) {
      this._emitChange(this.getReducers())
    }
  }
  getReducers() {
    return {...this._reducers}
  }
  setChangeListener(listener) {
    if (this._emitChange != null) {
      throw new Error('Can only set the listener for a ReducerRegistry once.')
    }
    this._emitChange = listener
  }
}

Create a registry instance when bootstrapping your app, passing in reducers which will be included in the entry bundle:

// coreReducers is a {name: function} Object
var coreReducers = require('./reducers/core')
var reducerRegistry = new ReducerRegistry(coreReducers)

Then when configuring the store and routes, use a function which you can give the reducer registry to:

var routes = createRoutes(reducerRegistry)
var store = createStore(reducerRegistry)

Where these functions look something like:

function createRoutes(reducerRegistry) {
  return <Route path="/" component={App}>
    <Route path="core" component={Core}/>
    <Route path="async" getComponent={(location, cb) => {
      require.ensure([], require => {
        reducerRegistry.register({async: require('./reducers/async')})
        cb(null, require('./screens/Async'))
      })
    }}/>
  </Route>
}

function createStore(reducerRegistry) {
  var rootReducer = createReducer(reducerRegistry.getReducers())
  var store = createStore(rootReducer)

  reducerRegistry.setChangeListener((reducers) => {
    store.replaceReducer(createReducer(reducers))
  })

  return store
}

Here's a basic live example which was created with this setup, and its source:

It also covers the necessary configuration to enable hot reloading for all your reducers.

Jonny Buchanan
  • 58,371
  • 16
  • 137
  • 146
6

There is now a module that adds injecting reducers into the redux store. It is called Redux Injector.

Here is how to use it:

  1. Do not combine reducers. Instead put them in a (nested) object of functions as you would normally but without combining them.

  2. Use createInjectStore from redux-injector instead of createStore from redux.

  3. Inject new reducers with injectReducer.

Here is an example:

import { createInjectStore, injectReducer } from 'redux-injector';

const reducersObject = {
   router: routerReducerFunction,
   data: {
     user: userReducerFunction,
     auth: {
       loggedIn: loggedInReducerFunction,
       loggedOut: loggedOutReducerFunction
     },
     info: infoReducerFunction
   }
 };

const initialState = {};

let store = createInjectStore(
  reducersObject,
  initialState
);

// Now you can inject reducers anywhere in the tree.
injectReducer('data.form', formReducerFunction);

Full Disclosure: I am the creator of the module.

Rich Warrior
  • 1,310
  • 6
  • 17
Randall Knutson
  • 351
  • 3
  • 3
4

As of October 2017:

  • Reedux

    implements what Dan suggested and nothing more, without touching your store, your project or your habits

There are other libraries too but they might have too many dependencies, less examples, complicated usage, are incompatible with some middlewares or require you to rewrite your state management. Copied from Reedux's intro page:

Silviu-Marian
  • 8,669
  • 5
  • 43
  • 68
2

We released a new library that helps modulating a Redux app and allows dynamically adding/removing Reducers and middlewares.

Please take a look at https://github.com/Microsoft/redux-dynamic-modules

Modules provide the following benefits:

  • Modules can be easily re-used across the application, or between multiple similar applications.

  • Components declare the modules needed by them and redux-dynamic-modules ensures that the module is loaded for the component.

  • Modules can be added/removed from the store dynamically, ex. when a component mounts or when a user performs an action

Features

  • Group together reducers, middleware, and state into a single, re-usable module.
  • Add and remove modules from a Redux store at any time.
  • Use the included component to automatically add a module when a component is rendered
  • Extensions provide integration with popular libraries, including redux-saga and redux-observable

Example Scenarios

  • You don't want to load the code for all your reducers up front. Define a module for some reducers and use DynamicModuleLoader and a library like react-loadable to download and add your module at runtime.
  • You have some common reducers/middleware that need to be re-used in different areas of your application. Define a module and easily include it in those areas.
  • You have a mono-repo that contains multiple applications which share similar state. Create a package containing some modules and re-use them across your applications
Code Ninja
  • 89
  • 1
  • 7
0

Here is another example with code splitting and redux stores, pretty simple & elegant in my opinion. I think it may be quite useful for those who are looking for a working solution.

This store is a bit simplified it doesn't force you to have a namespace (reducer.name) in your state object, of course there may be a collision with names but you can control this by creating a naming convention for your reducers and it should be fine.