These days, to achieve optimal app load times when users visit our website, we are questioning every byte of code that is being transferred on the network.

Let's say a user is visiting the homepage of an e-commerce website (react & redux). To achieve the best time to interactive, the javascript bundle should only have the UI components needed to render above-the-fold part of the homepage. We shouldn't load the code of product list or checkout before visiting those pages.

To achieve this you can:

lazy load routes - each route's UI components in on-demand bundles. lazy load the components below-the-fold of the page.

What about reducers?

Unlike components, the main bundle has all the reducers and not just the ones needed by homepage. The reasons why we couldn't do it were -

The best practice is to keep the redux state tree flat - no parent-child relationships between reducers to create a code-split point. The module dependency trees of components and reducers are not same. store.js -imports-> rootReducer.js -imports-> reducer.js(files) so the store's dependency tree contains all the reducers of the app even if the data stored is used by a main component or an on-demand component. Knowledge of which data is used in a component is business logic or at least isn't statically analyzable - mapStateToProps is a runtime function. Redux store API doesn't support code-splitting out of the box and all the reducers need to be part of the rootReducer before creation of the store. But wait, during development, whenever I update my reducer code my store gets updated via webpack's Hot Module Replacement. How does that work? Yes, for that we re-create rootReducer and use store.replaceReducer API. It's not as straightforward as switching a single reducer or adding a new one.

Came across any unfamiliar concepts? Please refer to the links and description below to gain a basic understanding of redux, modules and webpack.

Redux - a simple library to manage app state, core concepts, with react.

Modules - Intro, es6 modules, dynamic import

Dependency Tree - If moduleB is imported in moduleA , then moduleB is a dependency of moduleA and if moduleC is imported in moduleB , then the resultant dependency tree is - moduleA -> moduleB -> moduleC . Bundlers like webpack traverse this dependency tree to bundle the codebase.

is imported in , then is a dependency of and if is imported in , then the resultant dependency tree is - . Bundlers like webpack traverse this dependency tree to bundle the codebase. Code-Splitting - When a parent module imports a child module using a dynamic import, webpack bundles the child module and its dependencies in a different build file that will be loaded by the client when the import call is run at runtime. Webpack traverses the modules in the codebase and generates bundles to be loaded by browser.

Now you are familiar with the above concepts, let's dive in.

Let's look at the typical structure of a react-redux app -





// rootReducer.js export default combineReducers ({ home : homeReducer , productList : productListReducer }); // store.js export default createStore ( rootReducer /* , initialState, enhancer */ ); // Root.js import store from ' ./store ' ; import AppContainer from ' ./AppContainer ' ; export default function Root () { return ( < Provider store = { store } > < AppContainer /> </ Provider > ); }

First you create the rootReducer and redux store, then import the store into Root Component. This results in a dependency tree as shown below



RootComponent.js |_store.js | |_rootReducer.js | |_homeReducer.js | |_productListReducer.js |_AppContainer.js |_App.js |_HomePageContainer.js | |_HomePage.js |_ProductListPageContainer.js |_ProductListPage.js

Our goal is to merge the dependency trees of store and AppContainer -

So that when a component is code-split, webpack bundles this component and the corresponding reducer in the on-demand chunk. Let's see how the desired dependency tree may look like -





RootComponent.js |_AppContainer.js |_App.js |_HomePageContainer.js | |_HomePage.js | |_homeReducer.js |_ProductListPageContainer.js |_ProductListPage.js |_productListReducer.js

If you observe. you will notice that there is no store in the dependency tree!

In the above dependency tree

Say ProductListPageContainer is dynamically imported in AppContainer . Webpack now builds productListReducer in the on-demand chunk and not in the main chunk. Each reducer is now imported and registered on the store in a container.

Interesting! Now containers not only bind data & actions but reducers as well.

Now let's figure out how to achieve this!

Redux store expects a rootReducer as the first argument of createStore . With this limitation we need two things -

Make containers bind reducers before creation of the rootReducer

A higher order entity that can hold the definitions of all the reducers to be present in the rootReducer before they are packaged into one.

So let's say we have a higher-order entity called storeManager which provides the following APIs

sm.registerReducers()

sm.createStore()

sm.refreshStore()

Below is the refactored code & the dependency tree with storeManager -



// HomePageContainer.js import storeManager from ' react-store-manager ' ; import homeReducer from ' ./homeReducer ' ; storeManager . registerReducers ({ home : homeReducer }); export default connect ( /* mapStateToProps, mapDispatchToProps */ )( HomePage ); // ProductListPageContainer.js import storeManager from ' react-store-manager ' ; import productListReducer from ' ./productListReducer ' ; storeManager . registerReducers ({ productList : productListReducer }); export default connect ( /* mapStateToProps, mapDispatchToProps */ )( ProductListPage ); // AppContainer.js import storeManager from ' react-store-manager ' ; const HomeRoute = Loadable ({ loader : import ( ' ./HomePageContainer ' ), loading : () => < div > Loading... </ div > }); const ProductListRoute = Loadable ({ loader : import ( ' ./ProductListPageContainer ' ), loading : () => < div > Loading... </ div > }); function AppContainer ({ login }) { return ( < App login = { login } > < Switch > < Route exact path = "/" component = { HomeRoute } /> < Route exact path = "/products" component = { ProductListRoute } /> </ Switch > </ App > ); } export default connect ( /* mapStateToProps, mapDispatchToProps */ )( AppContainer ); // Root.js import storeManager from ' react-store-manager ' ; import AppContainer from ' ./AppContainer ' ; export default function Root () { return ( < Provider store = { storeManager . createStore ( /* initialState, enhancer */ ) } > < AppContainer /> </ Provider > ); }

Reducers are just registered and Store is created when RootComponent is being mounted. Now this has the desired dependency tree



RootComponent.js |_AppContainer.js |_App.js |_HomePageContainer.js | |_HomePage.js | |_homeReducer.js |_ProductListPageContainer.js |_ProductListPage.js |_productListReducer.js

Now if ProductListPageContainer is on-demand loaded using a dynamic import, productListReducer is also moved inside the on-demand chunk.

Hurray! mission accomplished?… Almost

Problem is, when the on-demand chunk is loaded -

sm.registerReducers() calls present in the on-demand chunk register the reducers on the storeManager but don't refresh the redux store with a new rootReducer containing newly registered reducers. So to update the store's rootReducer we need to use redux's store.replaceReducer API.

So when a parent ( AppContainer.js ) that is dynamically loading a child( ProductListPageContainer.js ), it simply has to do a sm.refreshStore() call. So that store has productListReducer , before ProductListPageContainer can start accessing the data or trigger actions on, the productList datapoint.



// AppContainer.js import { withRefreshedStore } from ' react-store-manager ' ; const HomeRoute = Loadable ({ loader : withRefreshedStore ( import ( ' ./HomePageContainer ' )), loading : () => < div > Loading... </ div > }); const ProductListRoute = Loadable ({ loader : withRefreshedStore ( import ( ' ./ProductListPageContainer ' )), loading : () => < div > Loading... </ div > }); function AppContainer ({ login }) { return ( < App login = { login } > < Switch > < Route exact path = "/" component = { HomeRoute } /> < Route exact path = "/products" component = { ProductListRoute } /> </ Switch > </ App > ); }

We saw how storeManager helps achieve our goals. Let's implement it -





import { createStore , combineReducers } from ' redux ' ; const reduceReducers = ( reducers ) => ( state , action ) => reducers . reduce (( result , reducer ) => ( reducer ( result , action ) ), state ); export const storeManager = { store : null , reducerMap : {}, registerReducers ( reducerMap ) { Object . entries ( reducerMap ). forEach (([ name , reducer ]) => { if ( ! this . reducerMap [ name ]) this . reducerMap [ name ] = []; this . reducerMap [ name ]. push ( reducer ); }); }, createRootReducer () { return ( combineReducers ( Object . keys ( this . reducerMap ). reduce (( result , key ) => Object . assign ( result , { [ key ]: reduceReducers ( this . reducerMap [ key ]), }), {})) ); }, createStore (... args ) { this . store = createStore ( this . createRootReducer (), ... args ); return this . store ; }, refreshStore () { this . store . replaceReducer ( this . createRootReducer ()); }, }; export const withRefreshedStore = ( importPromise ) => ( importPromise . then (( module ) => { storeManager . refreshStore (); return module ; }, ( error ) => { throw error ; }) ); export default storeManager ;

You can use the above snippet as a module in your codebase or use the npm package listed below -

sagiavinash / redux-store-manager Declaratively code-split your redux store and make containers own entire redux flow using redux-store-manager redux-store-manager Declaratively code-split your redux store and make containers own entire redux flow using redux-store-manager Installation yarn add redux-store-manager Problem rootReducer is traditionally created manually using combineReducers and this makes code-splitting reducers based on how widgets consuming their data are loaded(whether they are in the main bundle or on-demand bundles) hard. Bundler cant tree-shake or dead code eliminate the rootReducer to not include reducers whose data is not consumed by any container components Solution Let the containers that are going to consume the data stored by a reducer and trigger actions take responsibility of adding a reducer to the store This makes the container owning the entire redux flow by linking Actions as component props via mapDispatchToProps

as component props via Reducer responsible for updating the data via storeManager.registerReduers

responsible for updating the data via Data as component props via mapStateToProps Use the redux store's replaceReducer API whatever reducers are registered when an on-demand chunk loads the store gets refreshed… View on GitHub

Say hello to an untapped area of build optimizations :)

Like the concept? - Please share the article and star the git repo :)