Code-splitting with React Suspense, lazy + Router + Redux + Webpack 4

Code-splitting with React Suspense, lazy + Router + Redux + Webpack 4

  • 2018-11-24 03:20 AM
  • 179

The world is constantly changing and the digital side of it does not stand in a shadow. Regarding to the Front-end, “our world” is growing extremely fast. A few weeks ago Facebook team has provided us with a freshly new React release, were we got a lot of new stuffs.

Today we are going to dive deeper in the new features of React ^16 — Suspense and lazy and todiscuss the most interesting part: how tobind them with React de facto standardized state management library Redux. 💪 😁

In this article we will use already created App that you can clone from GitHub repo https://github.com/BiosBoy/React-Redux-Suspense-Lazy-Memo-test

What is that - Suspense and lazy?

In short, **Suspense **— is the feature that allows you to defer rendering part of your application tree until some condition is met (for example data from an endpoint or a resource is loaded). As about Lazy — it is a wrapper for dynamic import that comes to us from one of the last React releases. So, the example for usage is below:

import React, {lazy, Suspense} from 'react';
const DynamicComponent = lazy(() => import('./someComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DynamicComponent />
    </Suspense>
  );
}

Here we have someComponent modulethat must to be loaded asynchronously. So, to make this works we use React lazy feature wrapped by Suspense. These two are a sweet couple that must be always together, because first is responded for async module load and the second one for providing user with some spinner or loading info on the screen until async module loading is not finished. It’s prettier easy and useful.

Why do we need Suspense/Lazy?

More than one year has past from the moment when we got the Dynamic Imports feature. It was a great jump for all developers, who had a problem of how to make an app more lightweight and to deliver only parts of code that they really need to interaction to the costumers.

It introduces a new function-like form of import that caters to various use cases. The function below returns a promise for the module namespace object of the requested module, which is created after fetching, instantiating, or evaluating all of the module’s dependencies, as well as the module itself.

Here’s how to import and use some module in Vanilla JavaScript dynamically:

<script type="module">
  const moduleDynamic = './someModule.js';
  import(moduleDynamic)
    .then((module) => {
      module.doSomething();
    });
</script>

As for React developing, today we have a lot of custom solutions/libraries for React Dynamics imports, aimed to make our work with code-splitting easily and funny. On my opinion the most popular of them is a React-Loadable package, which provides us with a friendly API, look at it:

import React from 'react';
import Loadable from 'react-loadable';
const LoadableComponent = Loadable({
   loader: () => import('./someComponent'),
   loading: <div>...Loading</div>
});
class App extends React.Component {
   render() {
      return <LoadableComponent/>;
   }
}

It is pretty simple, is not it? It has been a very good approach for all of us until we got the native React out of the box support for code-splitting. So, I think today React-Loadable will lost its popularity, because Suspense/Lazy gives more flexibility and customization for regular React developing.

In coupling with such new introduced React features as scheduler and concurrent we have a capability to choose when and how we can provide user with interaction elements:

Before component load will be finished.

After component load will be finished.

Onload component phase.

Anyway, this all requires own articles with examples, so here we would not go deeper. Instead, with all info above we are finally ready to start creating React(Suspense/Lazy)-Redux-Router app.

Remember that we’ll use already created example as a test stand, which you can clone and play with for this article: https://github.com/BiosBoy/React-Redux-Suspense-Lazy-Memo-test

Starting Code-splitting with Webpack 4

So, the first step is to create right Webpack config that will allow us to make an App bundles based on the App huge Modules/Routes (in our case the last one is the most important).

I will not copy-past here a long list of used Webpack code config, instead I want to provide you only with the main part, which is responded for creating chunks of the our future app, and describe it. Here I will also give you a link on the Github gist of this config, where you can find how it is implemented and works in…

Here is where you can find this whole Webpack 4 config ready for work: https://gist.github.com/BiosBoy/8b45ef3fec246813ecb05ce1ae11bfde

// ...
const optimization = {
  optimization: {
    splitChunks: {
      chunks: ‘all’,
      minChunks: 2
    },
    // ...some other rules
  }
  // ... some other modules
}
// ...

As you can see above, the splitChunks rule is responded for bundles/chunks creation. It’s a main point to configure bundles of the App. It can be significantly customized, but for us the current config is exhaustive. It’ll give us an opportunity to create chunks if in app there are more than 2 dynamic modules/Components included (in our case they are Routes).

More info about splitChunks customization you can find on official Webpack page: https://webpack.js.org/plugins/split-chunks-plugin/ .

Creating App Routing with React-Routing

On the second step we need to create a right routing structure for our App. Here is where we will implement all we were talking above with the help of Suspense and lazy React ^16 features.

In this article we will work on a very simple counter App. It will consist of several parts:

--| components: 
  |-- <Header />
  |-- <Body /> 
  |-- <Footer />
--| container: 
  |-- <AppContainer />
--| layout:
  |-- <AppLayout />
--| routes:
  |-- <HelloWorld />
  |-- <StartCoding />
--| controller:
  |-- store.js
  |-- actions.js
  |-- reducers.js
  |-- initialState.js
  |--| middleware:
     |-- reduxLogger.js
     |-- rootReducer.js

…where Body Component is a HOC that can accepts 2 dynamic Components: <HelloWorld /> and <StartCoding />.

Important! I will not stop on how React ecosystem and Component import works (I guess if you are reading this article, it means you are already familiar with it). The point that I want to show you is how easily we can make React App Code-Splitting with Suspense/lazy, Webpack 4, Routing and Redux.

Let’s start to work and make an entry point of our App routing:

// ./container/index.js
// ... some Components and dependencies imports
const AppContainer = ({ store, history }) => {
  return (
    <AppLayout>
      <Suspense fallback={<LoadingPlaceholder />}>
        <Switch location={location}>
          <Route 
            exact 
            path='/'
            render={
              () => <AsyncComponent componentName='HelloWorld' />
            }
          />
          <Route 
            path='/next' 
            render={
              () => <AsyncComponent componentName='StartCoding' />
            } 
          />
       </Switch>
      </Suspense>
    </AppLayout>
  );
};
// ...

Here we see our main routing structure. It consists of react-router-dom v.4 API:

<Switcher /> for components replacement on url change.
 — <Route /> for components render based on current url.

And the most interesting for us

<Suspense /> wrapper, which provides a fallback API with opportunity to show user some placeholder before dynamic component will not be finally loaded. That means, each time when the bundle of the particular navigated is not loaded by user route, Suspense will fallback to show the placeholder. It’s easy and nice API, is not it? :)

Once bundle is loaded, Suspense will throw our dynamic Component via Render Pattern in <AppLayout /> App Layout Component.

But it’s not all. Suspense is only a fallback wrapper for dynamic loading. We must not forget about load dynamically HelloWorld and StartCoding Components in the App. We can make it real with React lazy wrapper. Here it is:

// ./routes/index.js
// ... some Components and dependencies imports
const HelloWorld = lazy(() => import(/* webpackChunkName: "HelloWorld" */ './HelloWorld'));
const StartCoding = lazy(() => import(/* webpackChunkName: "StartCoding" */ './StartCoding'));
const Components = {
  HelloWorld,
  StartCoding
};
const AsyncComponent = props => {
  const { componentName } = props;
  const Component = Components[componentName];
  return <Component {...props} />;
};
// ...

P.S. _/* webpackChunkName: ‘COMPONENT_NAME’*/_ allows us to set a bundle name instead of regular numbering them on app deployment stage.

And that’s all. Now we can couple all these together and put in the main layout Component:

class AppLayout extends Component {
  render() {
    const { children } = this.props;
    
    return (
      <div className={styles.appWrapper}>
        <Header />
        {children} // here is our dynamic component will be
        <Footer />
      </div>
    );
  }
}

Creating Redux store with asynchronous Reducers

The third step is very important, here we must somehow make our Redux store compatible with dynamic Components and provide each of them with the own reducers store. Today it’s not a problem. I’ll show here a very popular approach, based on the reducers injecting. Here is how it works:

  1. We need to create a basic Redux store:
// ./controller/store.js
// ... some Components and dependencies imports
const rootStore = () => {
  const middleware = [routerMiddleware(history), logger];
  const store = createStore(
    makeRootReducer(),
    initialState,
    compose(
      applyMiddleware(...middleware),
      ...enhancers
    )
  );
  store.asyncReducers = {};
  return store;
};
// ...

It’s a lot of code :), I know, but only one string is really important for us — store.asyncReducers . This injected object in Redux store will be responded about importing dynamic Component reducer.

2. Create App root reducer:

// ./controller/widdleware/rootReducer.js
// ... some dependencies imports
const makeRootReducer = asyncReducers => {
   return combineReducers({
     ...asyncReducers,
     common,
   });
};
// ...

What we have above — function makeRootReducer() that is a regular wrapper for combineReducers API from redux package. It can receive any count of new reducers (getting from dynamic components) and combine them all together in the main single store. But it’s not the end. We need some API to use this approach in the App.

So, to bring it works we can write a tiny utility function named injectReducer() in the same file with makeRootReducer :

// ./controller/widdleware/rootReducer.js
// ... some dependencies imports
const makeRootReducer = asyncReducers => {
  // ...
};
export const injectReducer = (store, { key, reducer }) => {
  if (Object.hasOwnProperty.call(store.asyncReducers, key)) return;
   
  store.asyncReducers[key] = reducer;
  store.replaceReducer(makeRootReducer(store.asyncReducers));
};
// ...

The injectReducer function will check if the dynamically loaded component is already in the store and will immediately break or add it if reducer is not there.

This is almost all! We just need to make only a few improvements in code and our App will be alive! :)

Integrating Redux store with React Dynamic Components

The final step is to make right Redux store integration between global Reducer and particular Reducers of HelloWorld and StartCoding routes.

In the App repo above that I provided on the beginning you can find how business logic is implemented between Components routes and global App store, as well. But, I assure you there is nothing interesting or innovative that cannot be found in the examples from the Redux website😉

Let’s go ahead and make our routes’ reducer injection inside whole Redux store. To make this, first of all, we need to upgrade our current routes’ code and insert injectReducer function, which we created before, after Component load:

// ./routes/index.js
const AsyncComponent = props => {
  //...
  import(`./${componentName}/controller/reducer`)
     .then(({ default: reducer }) => {
       injectReducer(rootStore, { key: componentName, reducer });
     })
  //... 
};
// ...

— Figure out what we were doing above: Here we see a regular JS Dynamic Import usage: 
import(./${componentName}/controller/reducer)

… we use it to make import of current Component reducer based on the componentName prop that we received during render from <AppLayout /> Component.

Once module reducer is loaded, we make an injection of it inside global Redux store rootStore with included key prop as a marker for checking if this reducer is already presented in the store:

 injectReducer(rootStore, { key: componentName, reducer });

So, by this tiny integration, we have found an easy way how to hold global and particular routes’ reducers in one place, which are accessible from the whole App context.

Integration React-Router with Redux store

The last thing that we need to implement in is just to make our Redux store responsive on location/route changing. All that we need to do — to wrap <AppContainer /> descendants in the redux <Prodiver /> and connected-react-router <ConnectedRouter /> packages HOC’s:

// ./container/index.js
// ... some Components and dependencies imports
const AppContainer = ({ store, history }) => {
  return (
    <AppLayout>
      <Prodiver store={store}>
        <ConnectedRouter history={history}>
         
          //... dependencies Routes/Components
        </ConnectedRouter>
      </Provider>      
    </AppLayout>
  );
};
// ...

Also, we need to remember about adding connected-react-router reducer with history object inside global Redux store in:

// ./controller/widdleware/rootReducer.js
// ... some dependencies imports
const makeRootReducer = asyncReducers => {
   return combineReducers({
      ...asyncReducers,
      common,
      // routing
      router: connectRouter(history)
   });
};
// ...

If you wonder how to get the _history_ object, _connectRouter_ reducer and combine them together, you can find answer in App repo to this post.

Wrapping Up

And here you got it! We have a simple, but cool React-Redux application with Code-Splitting based on pretty easy Webpack 4 configuration and Suspense/lazy dynamic Components loading logic.

You can now try to implement new Routes, Reducers and also play with Async Redux-Saga integration! It has the same pattern as described in this article! There are no bounds to make your app as you want it to see!

Of course, let me know if I got something wrong or if there is something that could be improved or simplified. PR’s and issues are more than welcome! Here’s some helpful links:

Thanks for reading. If you like this article, feel free to give it a clap or two. It’s quick, it’s easy, and it’s free!

30s ad

React Native for Mobile Developers

React Native for Mobile Developers

React Native: Advanced Concepts

React Native: Build Your Own Mobile Apps

React Redux React-Router: From Beginner to Paid Professional