Code Splitting in React using React.lazy and Loadable Components
Tags: react, suspense
Published: 2021-04-20 03:21:58

When our project grows and we add more functionalities, we end up adding a lot of code and libraries,
which result in a larger bundle size. A bundle size of a few hundred KBs might not feel a lot,
but in slower networks or in mobile networks it will take a longer time to load thus creating a bad user experience.

The solution to this problem is to reduce the bundle size.
But if we delete the large packages then our functionalities will be broken. So we will not be deleting the packages,
but we will only be loading the js code which is required for a particular page.
Whenever the user navigates or performs an action on the page, we will download the code on the fly,
thereby speeding up the initial page load.

When the Create React App builds the code for production, it generates only 2 main files:

1.  A file having react library code and its dependencies.
2.  A file having your app logic and its dependencies.

So to generate a separate file for each component or each route we can either make use of `React.lazy`,
which comes out of the box with react or any other third party library. In this tutorial, we will see both the ways.

{% youtube UbRdRRPbows%}

# Initial Project Setup

Create a react app using the following command:

```
npx create-react-app code-splitting-react
```

# Code splitting using React.lazy

Create a new component `Home` inside the file `Home.js` with the following code:

```jsx
import React, { useState } from "react"

const Home = () => {
  const [showDetails, setShowDetails] = useState(false)
  return (
    <div>
      <button
        onClick={() => setShowDetails(true)}
        style={{ marginBottom: "1rem" }}
      >
        Show Dog Image
      </button>
    </div>
  )
}

export default Home
```

Here we have a button, which on clicked will set the value of `showDetails` state to `true`.

Now create `DogImage` component with the following code:

```jsx
import React, { useEffect, useState } from "react"

const DogImage = () => {
  const [imageUrl, setImageUrl] = useState()
  useEffect(() => {
    fetch("https://dog.ceo/api/breeds/image/random")
      .then(response => {
        return response.json()
      })
      .then(data => {
        setImageUrl(data.message)
      })
  }, [])

  return (
    <div>
      {imageUrl && (
        <img src={imageUrl} alt="Random Dog" style={{ width: "300px" }} />
      )}
    </div>
  )
}

export default DogImage
```

In this component,
whenever the component gets mounted we are fetching random dog image from [Dog API](https://dog.ceo/dog-api/) using the `useEffect` hook.
When the URL of the image is available, we are displaying it.

Now let's include the `DogImage` component in our `Home` component, whenever `showDetails` is set to `true`:

```jsx
import React, { useState } from "react"
import DogImage from "./DogImage"
const Home = () => {
  const [showDetails, setShowDetails] = useState(false)
  return (
    <div>
      <button
        onClick={() => setShowDetails(true)}
        style={{ marginBottom: "1rem" }}
      >
        Show Dog Image
      </button>
      {showDetails && <DogImage />}
    </div>
  )
}
export default Home
```

Now include `Home` component inside `App` component:

```jsx
import React from "react"
import Home from "./Home"

function App() {
  return (
    <div className="App">
      <Home />
    </div>
  )
}

export default App
```

Before we run the app, let's add few css to `index.css`:

```css
body {
  margin: 1rem auto;
  max-width: 900px;
}
```

Now if you run the app and click on the button, you will see a random dog image:

![Random Dog Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ge4f2neby9n7g6w355e.JPG)

## Wrapping with Suspense

React introduced [Suspense](https://reactjs.org/docs/react-api.html#suspense) in version 16.6,
which lets you wait for something to happen before rendering a component.
Suspense can be used along with [React.lazy](https://reactjs.org/docs/react-api.html#reactlazy) for dynamically loading a component.
Since details of things being loaded or when the loading will complete is not known until it is loaded, it is called suspense.

Now we can load the `DogImage` component dynamically when the user clicks on the button.
Before that, let's create a `Loading` component that will be displayed when the component is being loaded.

```jsx
import React from "react"

const Loading = () => {
  return <div>Loading...</div>
}

export default Loading
```

Now in `Home.js` let's dynamically import `DogImage` component using `React.lazy` and wrap the imported component with `Suspense`:

```jsx
import React, { Suspense, useState } from "react"
import Loading from "./Loading"

// Dynamically Import DogImage component
const DogImage = React.lazy(() => import("./DogImage"))

const Home = () => {
  const [showDetails, setShowDetails] = useState(false)
  return (
    <div>
      <button
        onClick={() => setShowDetails(true)}
        style={{ marginBottom: "1rem" }}
      >
        Show Dog Image
      </button>
      {showDetails && (
        <Suspense fallback={<Loading />}>
          <DogImage />
        </Suspense>
      )}
    </div>
  )
}
export default Home
```

`Suspense` accepts an optional parameter called `fallback`,
which will is used to render a intermediate screen when the components wrapped inside `Suspense` is being loaded.
We can use a loading indicator like spinner as a fallback component.
Here, we are using `Loading` component created earlier for the sake of simplicity.

Now if you simulate a slow 3G network and click on the "Show Dog Image" button,
you will see a separate js code being downloaded and "Loading..." text being displayed during that time.

![Suspense Loading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/156vhejsol3tkllcw00r.png)

## Analyzing the bundles

To further confirm that the code split is successful, let's see the bundles created using `webpack-bundle-analyzer`

Install `webpack-bundle-analyzer` as a development dependency:

```bash
yarn add webpack-bundle-analyzer -D

```

Create a file named `analyze.js` in the root directory with the following content:

```js
// script to enable webpack-bundle-analyzer
process.env.NODE_ENV = "production"
const webpack = require("webpack")
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin
const webpackConfigProd = require("react-scripts/config/webpack.config")(
  "production"
)

webpackConfigProd.plugins.push(new BundleAnalyzerPlugin())

// actually running compilation and waiting for plugin to start explorer
webpack(webpackConfigProd, (err, stats) => {
  if (err || stats.hasErrors()) {
    console.error(err)
  }
})
```

Run the following command in the terminal:

```bash
node analyze.js
```

Now a browser window will automatically open with the URL http://127.0.0.1:8888

If you see the bundles, you will see that `DogImage.js` is stored in a different bundle than that of `Home.js`:

![Suspense Loading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0dtf79ezewi1y2c6t62.png)

## Error Boundaries

Now if you try to click on "Show Dog Image" when you are offline,
you will see a blank screen and if your user encounters this, they will not know what to do.

![Suspense No Network](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8elehmgnlpvqvksegpzw.png)

This will happen whenever there no network or the code failed to load due to any other reason.

If we check the console for errors, we will see that React telling us to add
[error boundaries](https://reactjs.org/docs/error-boundaries.html):

![Error Boundaries Console Error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3vhi52j7rzyib5rdtgw.png)

We can make use of error boundaries to handle any unexpected error that might occur during the run time of the application.
So let's add an error boundary to our application:

```jsx
import React from "react"

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true }
  }

  render() {
    if (this.state.hasError) {
      return <p>Loading failed! Please reload.</p>
    }

    return this.props.children
  }
}

export default ErrorBoundary
```

In the above class based component,
we are displaying a message to the user to reload the page whenever the local state `hasError` is set to `true`.
Whenever an error occurs inside the components wrapped within `ErrorBoundary`,
`getDerivedStateFromError` will be called and `hasError` will be set to `true`.

Now let's wrap our suspense component with error boundary:

```jsx
import React, { Suspense, useState } from "react"
import ErrorBoundary from "./ErrorBoundary"
import Loading from "./Loading"

// Dynamically Import DogImage component
const DogImage = React.lazy(() => import("./DogImage"))

const Home = () => {
  const [showDetails, setShowDetails] = useState(false)
  return (
    <div>
      <button
        onClick={() => setShowDetails(true)}
        style={{ marginBottom: "1rem" }}
      >
        Show Dog Image
      </button>
      {showDetails && (
        <ErrorBoundary>
          <Suspense fallback={<Loading />}>
            <DogImage />
          </Suspense>
        </ErrorBoundary>
      )}
    </div>
  )
}
export default Home
```

Now if our users click on "Load Dog Image" when they are offline, they will see an informative message:

![Error Boundaries Reload Message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l98p3d180o571ry1uwcq.png)

# Code Splitting Using Loadable Components

When you have multiple pages in your application and if you want to bundle code of each route a separate bundle.
We will make use of react router dom for routing in this app.
In my previous article, I have explained in [detail about React Router](https://www.codingdeft.com/posts/react-router-tutorial/).

Let's install `react-router-dom` and `history`:

```bash
yarn add react-router-dom@next history
```

Once installed, let's wrap `App` component with `BrowserRouter` inside `index.js`:

```jsx
import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./App"
import { BrowserRouter } from "react-router-dom"

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
)
```

Let's add some Routes and Navigation links in `App.js`:

```jsx
import React from "react"
import { Link, Route, Routes } from "react-router-dom"
import CatImage from "./CatImage"
import Home from "./Home"

function App() {
  return (
    <div className="App">
      <ul>
        <li>
          <Link to="/">Dog Image</Link>
        </li>
        <li>
          <Link to="cat">Cat Image</Link>
        </li>
      </ul>

      <Routes>
        <Route path="/" element={<Home />}></Route>
        <Route path="cat" element={<CatImage />}></Route>
      </Routes>
    </div>
  )
}

export default App
```

Now let's create `CatImage` component similar to `DogImage` component:

```jsx
import React, { useEffect, useState } from "react"

const DogImage = () => {
  const [imageUrl, setImageUrl] = useState()
  useEffect(() => {
    fetch("https://aws.random.cat/meow")
      .then(response => {
        return response.json()
      })
      .then(data => {
        setImageUrl(data.file)
      })
  }, [])

  return (
    <div>
      {imageUrl && (
        <img src={imageUrl} alt="Random Cat" style={{ width: "300px" }} />
      )}
    </div>
  )
}

export default DogImage
```

Let's add some css for the navigation links in `index.css`:

```css
body {
  margin: 1rem auto;
  max-width: 900px;
}

ul {
  list-style-type: none;
  display: flex;
  padding-left: 0;
}
li {
  padding-right: 1rem;
}
```

Now if you open the `/cat` route, you will see a beautiful cat image loaded:

![Cat Route](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flqdgcvuiokxo2bzfio6.png)

In order to load the `CatImage` component to a separate bundle, we can make use of [loadable components](https://loadable-components.com/).
Let's add `@loadable-component` to our package:

```bash
yarn add @loadable/component
```

In `App.js`, let's load the `CatImage` component dynamically using `loadable` function,
which is a default export of the loadable components we installed just now:

```jsx
import React from "react"
import { Link, Route, Routes } from "react-router-dom"
import Home from "./Home"
import loadable from "@loadable/component"
import Loading from "./Loading"

const CatImage = loadable(() => import("./CatImage.js"), {
  fallback: <Loading />,
})

function App() {
  return (
    <div className="App">
      <ul>
        <li>
          <Link to="/">Dog Image</Link>
        </li>
        <li>
          <Link to="cat">Cat Image</Link>
        </li>
      </ul>

      <Routes>
        <Route path="/" element={<Home />}></Route>
        <Route path="cat" element={<CatImage />}></Route>
      </Routes>
    </div>
  )
}

export default App
```

You can see that even `loadable` function accepts a fallback component to display a loader/spinner.

Now if you run the application in a slow 3G network,
you will see the loader and js bundle related to `CatImage` component being loaded:

![Loadable Component Loading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yqx8ck20xtn2d9dvcqnv.png)

Now if you run the bundle analyzer using the following command:

```bash
node analyze.js
```

You will see that `CatImage` is located inside a separate bundle:

![CatImage Bundle Analyzer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7n3pfjqxzx0c6z426uyq.png)

> You can use `React.lazy` for Route based code splitting as well.

# Source code and Demo

You can view the complete [source code here](https://github.com/collegewap/code-splitting-react) and a [demo here](https://code-splitting-react.vercel.app/).