user3139545
user3139545

Reputation: 7374

Adding reducers in middleware from multiple locations in Redux

I have generated a project using Create React App and then added Redux.

The redux state is then split into three parts that each has its own reducer and some middleware defined. The reducers are place in files called part1.js part2.js part3.js there is then a common.js file that imports the reducer and the middleware from part1-2-3.js and adds them to combineReducer and applyMiddeware.

My question is if there is anyway to not having to import everything in one place. What I want is to be able to add the reducer and middeware to comineReducer and applyMiddleware from within part1-2-3.js, the reason is to get rid of an explicit common boilerplate code file in common.js. Is this possible or is the only way to import everything into one place?

UPDATE

I have now great examples on how to solve the combineReducer part, however I still need to do something similar for applyMiddleware. I have found an example from the following repo on how to do something similar with applyMiddleware. However its in TypeScript and I have a hard time translating it into what is the minimal way to get this working within a JS React/Redux application. Would be great with some examples.

UPDATE

So I finally found this minimal library doing what I want.

Upvotes: 0

Views: 1101

Answers (2)

Peter
Peter

Reputation: 1822

Yes you can. We use it with dynamic import and works well. We use with react hooks.

use store.replaceReducer https://redux.js.org/api/store#replacereducernextreducer

in configureStore (or any file when you call createStore from redux)

const store = createStore(/*...*/)
add
store.injectedReducers = {}; // Reducer registry

and create a new file with injectReducer hook.

    const useInjectReducer reducer => {
      const store = useStore();
      const key = Object.keys(reducer)[0];
       if (
            Reflect.has(store.injectedReducers, key) &&
            store.injectedReducers[key] === reducer[key]
        ) {
            return;
        }

        store.injectedReducers = {
            ...store.injectedReducers,
            ...reducer
        };
        store.replaceReducer(combineReducers(store.injectedReducers));

    }

and you can use it in react App:

    export const TodoPage = () => {
        useInjectReducer({ [REDUCER_NAME]: TodoReducer });
        /*...*/
    }

if you use server side rendering you need to be sure redux not cleaning up the states for missing reducers before dynamic import. You can create a dummyReducers to prevent that.

 const dummyReducers = Object.keys(initialState).reduce((acc, current) => {
        acc[current] = (state = null) => state
        return acc;
    }, {});

and add this for:

const store = createStore(
    combineReducers(dummyReducers),
    initialState
) 

We use the same pattern to Inject Sagas.

Upvotes: 1

Adam Jenkins
Adam Jenkins

Reputation: 55643

Yes! I have a reducer registry which is similar (almost identical) to this reducer manager: https://redux.js.org/recipes/code-splitting#using-a-reducer-manager:

const DEFAULT_REDUCER = state => state || null;

export class ReducerRegistry {
  constructor() {
    this._emitChange = null;

    this._reducers = {};
  }

  getReducers() {
    // default reducer so redux doesn't complain
    // if no reducers have been registered on startup
    if (!Object.keys(this._reducers).length) {
      return { __: { reducer: DEFAULT_REDUCER } };
    }

    return { ...this._reducers };
  }

  register(name, reducer, options = {}) {
    if (this._reducers.name && this._reducers.name !== reducer) {
      throw new Error(`${name} has already been registered`);
    }

    this._reducers = { ...this._reducers, [name]: { reducer, options } };
    if (this._emitChange) {
      this._emitChange(this.getReducers());
    }
  }

  setChangeListener(listener) {
    this._emitChange = listener;
  }
}

const reducerRegistry = new ReducerRegistry();
export default reducerRegistry;

Then I have my redux domains organized into folders like reducks-style: https://github.com/alexnm/re-ducks

In the index.js of the reducks domain, I import the reducer registry and register the reducer:

import Domain from './name';
import reducer from './reducer';

import { reducerRegistry } from '...wherever';
reducerRegistry.register(Domain, reducer); // register your reducer

export { ... whatever }

Finally, my store uses the reducer registry like this:

export const store = createStore(
  combine(reducerRegistry.getReducers()),
  initialState,
  composeEnhancers(applyMiddleware(...middlewares))
);

// Replace the reducer whenever a new reducer is registered (or unregistered).!!!!
// THIS IS THE MAGIC SAUCE!
reducerRegistry.setChangeListener(reducers =>
  store.replaceReducer(combine(reducers))
);

export default store;

This setup has worked magically for us. Allows us to keep all of our redux logic very isolated from the rest of the application (and from other redux domain logic!), works fantastic for code-splitting. Highly recommend it.

Upvotes: 1

Related Questions