loading...
Cover image for How to code split Redux store to further improve your app's performance

How to code split Redux store to further improve your app's performance

websavi profile image Sagi Avinash Varma ・5 min read

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:

  1. lazy load routes - each route's UI components in on-demand bundles.
  2. 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 - 

  1. The best practice is to keep the redux state tree flat - no parent-child relationships between reducers to create a code-split point.
  2. 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.
  3. Knowledge of which data is used in a component is business logic or at least isn't statically analyzable - mapStateToProps is a runtime function.
  4. 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.
  • 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. code splitting

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

  1. Say ProductListPageContainer is dynamically imported in AppContainer. Webpack now builds productListReducer in the on-demand chunk and not in the main chunk.
  2. 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 - 

GitHub logo 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

  1. 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.
  2. Bundler cant tree-shake or dead code eliminate the rootReducer to not include reducers whose data is not consumed by any container components

Solution

  1. 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
    • Reducer responsible for updating the data via storeManager.registerReduers
    • Data as component props via mapStateToProps
  2. Use the redux store's replaceReducer API whatever reducers are registered when an on-demand chunk loads the store gets refreshed…

Say hello to an untapped area of build optimizations :)

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

Discussion

pic
Editor guide
Collapse
kepta profile image
Kushan Joshi

Hey Sagi great work on lazy loading reducers!

What are the benfits / performance gains that you are seeing in a production app? I ask this because I believe reducers are generally light weight and donot have a huge dependency tree that could benefit from lazy loading.

Collapse
websavi profile image
Sagi Avinash Varma Author

Actually my production app is a buisiness intelligence app. It involves lot of data stitching happening inside the reducer from multiple api calls. And I try to keep the data flat by normalizing the api responses. So for my usecase i need towrite lot of data transformation utilities.
If your app has so many routes then you should think of code splitting the redux dtore sine the total size contribution from sll routes would be huge

Collapse
chasahodge profile image
Charles Hodge

So I'm trying to implement your example within my own project and the webpack complier is giving me an error on the individual import statements for my ClientContainer and the CategoryContainer in the AppContainer.

loader: withRefreshedStore(import('./ClientContainer'))

Everything is implemented just as you laid out in this example, just altered for my app. Any ideas?

Collapse
meet_zaveri profile image
Meet Zaveri

Sounds solid. Will look through for future projects!

Collapse
navneetg profile image
Navneet Gupta

Great article Sagi, we recently opensourced our library to help in code splitting react redux apps, it is built on similar principels, please try it out.
github.com/Microsoft/redux-dynamic...

Collapse
omarassadi profile image
omar assadi

Very cool Sagi! thanks a lot for sharing this!

In the example you are importing the storeManager in AppContainer but not using it.

Collapse
cherryberry23 profile image
cherryBerry23

How can we retain the data from dynamic reducers on browser refresh