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.