question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC - API for dynamically adding reducers

See original GitHub issue

See discussion in issue #1126 for context.

Problem: Because ReactReduxContext is not part of the public API, there is currently no way using only public APIs to safely support dynamic loading of reducers. Even using this, implementation is tricky and error prone and there is also room to create a better story around code splitting reducers.

Proposed solution: User provides a getRootReducer(newReducers)-function to <Provider>. New component <WithReducers reducers={newReducers}> can be used to wrap a subtree that needs these reducers.


Summary of problem (recap of #1126)

react-redux version 6 now uses the new context instead of the legacy one, and instead of every connected component being subscribed directly to the store, only the root <Provider> is. That is:

  • Before: Every connected component called getState()
  • After: Only <Provider> calls getState(), state gets passed along via context and does not change during a single render

This is done to avoid “tearing” in concurrent rendering mode (avoid different parts of application rendering with different state).

In a world with concurrent rendering, replaceReducer is trickier to use from within the render tree. This is because a call to this function modifies the state in the store and subsequent components expect this state to be available, but now no longer fetches the latest state automatically, but instead from context.

On top of fetching store from the context and calling replaceReducer on it, the solution from #1126 is to patch the current storeState from context with the state added by the new reducers by wrapping that subtree in a new <ReactReduxContext.Provider>. However, ReactReduxContext is not part of the public API.

In a sense this is not a problem new to v6, since several of the old solutions for code splitting reducers in React relied on fetching store directly from context, which was never part of the public API.

On top of this, I feel like code splitting is something to be encouraged and there is room to improve the story around this and that this aligns with the goals of react-redux.

Proposed solution

Please note that this is just a rough sketch of what a solution could look like. I’d love feedback and to get a discussion going around both large issues and small.

Rough implementation and example usage in CodeSandbox - This is forked from @rgrove’s excellent repro-case from #1126

My hope is that this solution strikes a balance between being immediately useful and preserving the flexibility for the user to construct a root-reducer and injection techniques however they feel like.

Goals:

  • Add API for adding reducers from the component tree
  • Do not require direct use of store or ReactReduxContext in userland
  • Flexible API that supports both simple and complex usecases well and can be built on top of
  • Small surface area
  • Aligned with react-redux v6 approach for preparing for concurrent-safeness

Example usage

(Some imports etc omitted for brevity)

import { WithReducers } from 'react-redux';
import reducer from './namesReducer';

function Greeter({ currentName }) {
    return `Hi ${currentName}!`;
}

const mapStateToProps = state => ({
    currentName: state.names.currentName
});

const ConnectedGreeter = connect(mapStateToProps)(Greeter);

const newReducers = {
    names: reducer
};

export default props => (
    <WithReducers reducers={newReducers}>
        <ConnectedGreeter {...props} />
    </WithReducers>
);
import staticReducers from './staticReducers';

// Example simple userland implementation
let reducers = { ...staticReducers };
function getRootReducer(newReducers) {
    reducers = { ...reducers, ...newReducers };
    return combineReducers(reducers);
}

const DynamicGreeter = React.lazy(() => import('./Greeter'));

ReactDOM.render(
    <Provider store={store} getRootReducer={getRootReducer}>
        <Suspense fallback={<div>Loading...</div>}>
            <DynamicGreeter />
        </Suspense>
    </Provider>

API

Pretty much what you see in the example above, with the addition that <WithReducers> also takes the boolean prop replaceExisting which determines if existing reducers/state gets overwritten. Defaults to false.

Could also add other things, such as optionally removing reducers/state on unmount.

Unanswered questions

  • Naming (Is getRootReducer really a good name considering it’s basically expected to have sideeffects? Should WithReducer be called something else? Etc…)
  • When we have a component like this, it’s easy to add a HOC based on it, should this be part of the official implementation? Component version leaves forwardRef and hoisting statics to userland, HOC could do this behind the scenes.
  • This solution does not take into account other sideeffects such as adding sagas etc. Might be possible to add those in userland in getRootReducer since you often have access to store there, but then it should be renamed. Not sure if it’s in scope for this RFC.
  • Optionally removing reducers on unmount?

Probably a lot of questions I haven’t thought about, please feel free to add more!

Alternative solutions

Status quo - Determine that it is fine that these solutions rely on non-public APIs

Not desirable. Code splitting should not break between versions.

Make ReactReduxContext part of public API - Leave implementation to userland

This is a perfectly valid way to go. It does leave a lot of complexity to userland though and future updates to React & Redux might very well break implementations unintentionally. Using it directly is often a footgun, so should be discouraged.

Add escape hatch for tearing - As proposed in #1126

This was a proposal to opt-in to the old behaviour (components fetching directly from store) for first render via a prop on <Provider>, I feel this is the wrong way to go since it doesn’t tackle the problem directly and is probably a footgun. Was abandoned in #1126.

Add same functionality directly to existing connect-HOC - Add a new option to connect

Probably a viable alternative. Kind of a connect and make sure these extra reducers are present. Could either update the storeState on context so all following connects don’t have to add it explicitly, or require every connect that depends on these reducers to add them explicitly. This would only apply to state, either way the reducers would be added to the store on the first connect.

Different API from the proposed one - Based around same solution

If you have better ideas for how the public API for this could look like, I’d love it! The proposed solution is a rough one to get discussions going.

Something else? - There might very well be solutions I have not thought about.


This is by no means a finished or polished suggestion, I’d love to get a discussion going to improve it! Would it solve your usecase? It is a good fit to include in react-redux? Etc…

I’d be happy to submit this in PR-form instead if that would be helpful, but wanted to start as an issue to encourage discussion.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:9
  • Comments:20 (17 by maintainers)

github_iconTop GitHub Comments

1reaction
Ephemcommented, Dec 31, 2018

I totally agree with you that With... as a component name is jarring, I never liked it and added it mostly for clarity in the RFC around what the component is supposed to do. Now that we’ve come this far, I would suggest that the top function would be called modifyStore (verb), the component be called StoreModifier (noun) and the HOC withModifiedStore.

I really like the idea of a chain! Only problem that would need to be solved is clashing options I guess. What happens if you add several modifiers that unknowingly uses the same option-keys? I’m sure there would be a number of ways to solve that by namespacing though.

I also agree that passing the store, or a (possibly stripped down?) StoreAPI is the better option.

That EDIT is exactly where I was hoping to bring this RFC! Making it possible and hopefully easier to implement both existing and new solutions on top of.

I’m starting to have a good idea what an updated RFC would look like, but I don’t want to edit the original comment since that would cause confusion for new readers. I think at this point (after initial feedback) it makes sense to instead open a PR with the updated RFC (and possibly an example implementation) to help further discussion and so we can track changes.

I might leave the chain out of the original PR just to keep it simple at first, in that case with the intention of adding it. I’ll write it up ASAP.

Thank you so much for your comments @mpeyper and @abettadapur! Do you have any thoughts on this new approach @abettadapur?

1reaction
mpeypercommented, Dec 31, 2018

I like the look of this new approach.

With a little bit of work, the store modifier could be made into a modifier chain (similar to redux’s middleware chain) that would allow more modular store modifications to occur. I think passing through the whole store (or a storeAPI?) would make the chain concept a bit nicer too (especially for libraries that enhance the store with additional functions, such as redux-dynostore and redux-dynamic-modules).

Setting it up might look something like (stolen heavily from applyMiddleware):

let modifyStore = () => {
  throw new Error('not yet!');
}

const chain = modifiers.map(modifier => modifier({ ...store, modifyStore }));
modifyStore = compose(...chain)(() => {}); // `modifyStore` without any modifiers does nothing.

Then you would invoke it using the options provided from WithModifiedStore:

modifyStore(options);

Now we can split the modifiers into nice pieces, and even have modifiers call other modifiers. e.g:

const reducers = { ...staticReducers };
const dynamicReducers = store => next => options => {
  reducers = { ...reducers, ...options.reducers };
  store.replaceReducer(combineReducers(reducers));
  next(options);
}

const sagas = { ...staticSagas };
const dynamicSagas = (sagaMiddleware) => () => next => options => {
  const newSagaKeys = Object.keys(options.sagas).filter(key => ! sagas.includes(key));
  
  Object.keys(options.sagas)
    .filter(key => !runningSagas.includes(key))
    .forEach(key => {
      const saga = options.sagas[key];
      sagaMiddleware.run(saga);
      sagas[key] = saga;
    });

  next(options);
}

const dynamicModules = ({ addModules }) => next => options => {
  addModules(options.modules); // I have no idea if redux-dynamic-module handles adding the same module multiple times, but this is just for demonstration purposes
  next(options);
}

// dumb example
const conditionalReducers = (predicate, reducers) => store => next => options => {
  if (predicate(store)) {
    store.modifyStore({ reducers });
  }
  next(options);
}

const storeModifiers = [
  dynamicReducers,
  dynamicSagas(sagaMiddleware),
  dynamicModules,
  conditionalReducers(({ getState }) => getState().user.isAdmin, { adminSettings })
];

ReactDOM.render(
  <Provider store={store} storeModifiers={storeModifiers}>
    <App />
  </Provider>
);

Like middleware, the order in which the are provided would determine the order they are invoked (e.g. to ensure the reducers get added before the sagas are run). I’ve probably got the order wrong above, but 🤷‍♂️.

NOTE: this was all written in github editor and not tested. I’m always a little iffy when trying to use things like compose so there may need to be some tweeks and before it works.


I find the WithModifiedStore (and With... as a component name in general) a bit jarring as the thing it’s providing the modified store to might be a deeply nested child or may not even exist at all if it’s just running a saga. I think a small change to ModifyStore would be better (open to other ideas too. Making it an option in the connect HOC also fixes this, but is less flexible than having an intermediate component, which I’m coming around to as being the better idea as otherwise we may start to see components being connected just to add store modifications (i.e. not mapping any state or dispatches to use).


EDIT

I just realised that this suggestion is starting to look a lot like redux-dynostore’s enhancers. I could envision @redux-dynostore/core (the thing that adds all the extra functions to the store) not changing at all and @redux-dynostore/react-redux exporting a bunch of modifiers to use with Provider and dynamic just being a thin wrapper around the WithModifiedStore component, ensuring the provided things went into the correct keys of the options for the modifier to pick up.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Code Splitting | Redux
To code split with Redux, we want to be able to dynamically add reducers to the store. However, Redux really only has a...
Read more >
How to dynamically load reducers for code splitting in a Redux ...
My application consists of a lot of parts (pages, components) so I want to create many reducers. Redux examples show that I should...
Read more >
Create a guided tour plugin in the admin panel - Strapi
Learn how you can create a guided tour plugin in the admin panel using the reactour package.
Read more >
Jobs - AWS Elemental MediaConvert API Reference
When you want to add Dolby dynamic range compression (DRC) signaling to your output ... Specification to use (RFC-6381 or the default RFC-4281)...
Read more >
RFC Errata Report
reason for this provison is to allow future dynamic update facilities to ... This command should generate a "Subject" line which is the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found