0

The goal

I want to change reducers (and feed them new state) when the route changes.

Current Attempt

I have a route set up with a slug (/components/:slug) that is handled by my ComponentPage reducer. This reducer notices that the route changed (via Redux-saga) and has an opportunity to fetch state that's relevant to the current :slug.

When the router first updates to go to a /components page, the state looks something like this:

route: {...}
language: {...}
theme: {...}
componentPage: {
  content: {[default / empty]}
}

After ComponentPage fetches data related to the :slug, it looks something like this:

route: {...}
language: {...}
theme: {...}
componentPage: {
  content: {...}
  liveExample: {...}
  tabCollection: {...}
}

In the above state tree, items like liveExample and tabCollection represent new domains that are managed by their own reducers (and have their initial state set by componentPageReducer).

My intention is that items like these will be set dynamically by ComponentPage based on the page's :slug so that they can be swapped out for other components without littering the state tree with every possible component that could go on every possible instance of a component page.

The issue

I currently have configuration files set up to import the specific reducers each page needs so that they can feed them to ComponentPage based on the :slug. Unfortunately, my current method of implementation only suffices to care for the initial render—after that pass-through, the imported reducers are replaced by the objects they return.

An example of an object I'm currently handing to ComponentPage

pageConfiguration = {
  content: {...},
  liveExample: (state, action) => liveExampleReducer(state || liveExampleConfiguration, action),
  tabCollection: (state, action) => tabCollectionReducer(state || tabCollectionConfiguration, action),
}

A note on how I came to this:

Redux's combineReducers would return something of this structure:

liveExample: liveExampleReducer(liveExampleConfiguration, action),
tabCollection: tabCollectionReducer(tabCollectionConfiguration, action),

That would be ideal (if I just knew how to get these listed as proper reducers). Instead, I had to make the function callable so that I could pass in the action (when componentPageReducer doesn't match an action.type, it still tries to call these dynamically loaded reducers manually, as you'll see below). (They also accept state || configuration so that when these get called subsequent times, they don't keep reinitializing with configuration/initial data).

ComponentPage/reducer.js

export default function componentPageReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_SLUG:
      // gets the `pageConfiguration` object from above,
      // and generates the state by manually mapping through
      // its functions and calling them
      if (pages.has(action.payload.slug)) {
        return pages.get(action.payload.slug).map((x) => {
          if (typeof x === 'function') {
            return x(undefined, action)
          }
          return x
        })
      }
      return state
    default:
      // an attempt at calling the now-nonexistent
      // imported reducer functions
      return state.map((x) => {
        if (typeof x === 'function') {
          return x(state, action)
        }
        return x
      })
  }
}

Possible solutions

So, from what I can tell, this might work if I store the reducer functions themselves in the state and map their return value onto a different key so they don't replace themselves, but... it seems like I'm going down a weird road here.

I think that I am currently hacking together something that probably shouldn't be considered proper Reducer Composition. I don't know if there is a more elegant way to handle this sort of scenario.

aminimalanimal
  • 417
  • 2
  • 6
  • 13

1 Answers1

1

The standard approach to dynamic reducer slices is to keep around an object of all the existing slice reducers, and when you need to add a new one at runtime, update that object and re-run `combineReducers. To give you the basic idea:

let sliceReducers = {
    first : firstReducer,
    second : secondReducer
};

const initialRootReducer = combineReducers(sliceReducers);
const store = createStore(initialRootReducer);


// sometime later

sliceReducers = {
    ...sliceReducers,
    third : thirdReducer
}

const newRootReducer = combineReducers(sliceReducers);
store.replaceReducer(newRootReducer);

I have a couple articles on the topic in the Redux Techniques#Reducers section of my React/Redux links list. You may also want to look at the injectAsyncReducer utility from the React-Boilerplate toolkit.

markerikson
  • 63,178
  • 10
  • 141
  • 157