Code Splitting with React Router v6, React Lazy and Suspense (in simple terms)

omogbai

Omogbai Atakpu

Posted on July 2, 2022

Code Splitting with React Router v6, React Lazy and Suspense (in simple terms)

React and SPAs
The React framework is known for building single page applications (SPAs) out of separate components or modules. How it does this is through a ‘bundling’ process, where various components are imported from their files and merged into a single file, or bundle. This single file is added to a webpage and is loaded on a user's browser as an application.

Code Splitting - What does this mean?
When building an application, it is important to keep the bundle size as small as possible. This is because a large file can take pretty long for the browser to paint or load, especially in areas with poor internet connectivity, negatively affecting your web vitals and user experience.
For small applications, this is not an issue. But as the size of your application grows and the number of libraries and frameworks used increases, there is a need to split the bundle on the client side. This is called client side code splitting.

There are a few manual ways to code split with Webpack, Rollup, Browserify and other bundling tools. But React has provided features to help tackle this called: React.Lazy and Suspense.

Paraphrased from the official React documentation:

React.Lazy lets you render a dynamic import as a regular component. It takes a function that calls a dynamic import() and returns a Promise which resolves to a module with a default export containing a React Component.

This lazy component must also be rendered in a Suspense component; which provides fallback content (a React element) to show while the lazy component is loading.

Let’s take an example, where we'll use React Router v6 for client-side routing. We'll build a basic student dashboard to show course list and course scores.

This is how it'll look when we're done:

Dashboard demo

First, we create a new react project with Create-React-App. I’m using typescript so I’ll run:

npx create-react-app my-app --template typescript

npm i react-router-dom

This is how my App.tsx file looks:

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

And my index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Enter fullscreen mode Exit fullscreen mode

The Dashboard Page:

import React from "react";
import { Link, Outlet } from "react-router-dom";

const Dashboard = () => {
  return (
    <div style={{ padding: "1rem" }}>
      <h1>Dashboard Header</h1>
      <hr style={{ borderWidth: 1 }} />
      <Link to="/courses" style={{ marginBottom: "1rem" }}>
        View your courses
      </Link>
      <br />
      <Link to="/results">Check your results</Link>
      <Outlet />
    </div>
  );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

The Courses page:

import React from "react";

const UserCourses = () => {
  return (
    <div style={{ padding: "1rem" }}>
      <h4>Your Courses</h4>
      <ul>
        <li>Mathematics</li>
        <li>English</li>
        <li>Physics</li>
        <li>History</li>
      </ul>
    </div>
  );
};

export default UserCourses;
Enter fullscreen mode Exit fullscreen mode

The Results' page:

import React from "react";

type resultsType = {
  course: string;
  score: number;
  comments: string;
};

const UserResults = () => {
  const results: resultsType[] = [
    {
      course: "Mathematics",
      score: 50,
      comments: "Pass",
    },
    {
      course: "English",
      score: 67,
      comments: "Good",
    },
    {
      course: "Physics",
      score: 75,
      comments: "Good",
    },
    {
      course: "History",
      score: 85,
      comments: "Excellent",
    },
  ];

  return (
    <div style={{ padding: "1rem" }}>
      <h4>Your Results</h4>
      <table>
        <thead>
          <tr>
            <th style={{ textAlign: "start" }}>Course</th>
            <th style={{ padding: "0.5rem 1rem" }}>Score</th>
            <th>Comments</th>
          </tr>
        </thead>
        <tbody>
          {results.map((person: resultsType, id: number) => {
            const { course, score, comments } = person;

            return (
              <tr key={id}>
                <td>{course}</td>
                <td style={{ padding: "0.5rem 1rem" }}>{score} 
                </td>
                <td>{comments}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default UserResults;
Enter fullscreen mode Exit fullscreen mode

Now, to implement React Router.
I have added 'Browser Router' to index.tsx here:

...
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>
Enter fullscreen mode Exit fullscreen mode

Then we can import those pages into our App.tsx:

...
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route path="/courses" element={<UserCourses />} />
        <Route path="/results" element={<UserResults />} />
      </Route>
      <Route
        path="*"
        element={
          <div style={{ padding: "1rem" }}>
            <h3>Page Not Found!</h3>
          </div>
        }
      />
    </Routes>
Enter fullscreen mode Exit fullscreen mode

At the moment, we are done with step 1. This is a basic page that routes as required, but there's no lazy-loading here yet.

To utilise React.lazy() and Suspense we need to dynamically import the pages.

// import dynamically
const UserCourses = React.lazy(() => import("./pages/UserCourses"));
const UserResults = React.lazy(() => import("./pages/UserResults"));
Enter fullscreen mode Exit fullscreen mode

And I'll add a Suspense component with a fallback:

<Suspense
  fallback={
   <div className="loader-container">
    <div className="loader-container-inner">
     <RollingLoader />
    </div>
   </div>
   }
  >
  <UserCourses />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

App.tsx has become:

...
     <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route
          path="/courses"
          element={
            <Suspense
              fallback={
                <div className="loader-container">
                  <div className="loader-container-inner">
                    <RollingLoader />
                  </div>
                </div>
              }
            >
              <UserCourses />
            </Suspense>
          }
        />
        <Route
          path="/results"
          element={
            <Suspense
              fallback={
                <div className="loader-container">
                  <div className="loader-container-inner">
                    <RollingLoader />
                  </div>
                </div>
              }
            >
              <UserResults />
            </Suspense>
          }
        />

        {/* <Route path="/courses" element={<UserCourses />} />
        <Route path="/results" element={<UserResults />} /> */}
      </Route>
      <Route
        path="*"
        element={
          <div style={{ padding: "1rem" }}>
            <h3>Page Not Found!</h3>
          </div>
        }
      />
    </Routes>
Enter fullscreen mode Exit fullscreen mode

This means on initial paint, the browser will not load those pages until a user clicks the link. The user will only see a loading icon while the page is being loaded, this is our fallback content. Upon completion the page's content will display. This only occurs on initial paint and will not occur again.

We now have a component that loads lazily. However, this code is pretty repetitive and can be optimised even further by building a Suspense Wrapper that accepts the page's path as a prop.

The Suspense Wrapper:

import React, { Suspense } from "react";

import { ReactComponent as RollingLoader } from "../assets/icons/rolling.svg";

interface SuspenseWrapperProps {
  path: string;
}

const SuspenseWrapper = (props: SuspenseWrapperProps) => {
  const LazyComponent = React.lazy(() => import(`../${props.path}`));

  return (
    <Suspense
      fallback={
        <div className="loader-container">
          <div className="loader-container-inner">
            <RollingLoader />
          </div>
        </div>
      }
    >
      <LazyComponent />
    </Suspense>
  );
};

export default SuspenseWrapper;
Enter fullscreen mode Exit fullscreen mode

And finally, our App.tsx will look like this:

import React from "react";
import { Route, Routes } from "react-router-dom";

import "./App.css";
import Dashboard from "./pages/Dashboard";
import SuspenseWrapper from "./components/SuspenseWrapper";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route
          path="/courses"
          element={<SuspenseWrapper path="pages/UserCourses" />}
        />
        <Route
          path="/results"
          element={<SuspenseWrapper path="pages/UserResults" />}
        />

        {/* <Route path="/courses" element={<UserCourses />} />
        <Route path="/results" element={<UserResults />} /> */}
      </Route>
      <Route
        path="*"
        element={
          <div style={{ padding: "1rem" }}>
            <h3>Page Not Found!</h3>
          </div>
        }
      />
    </Routes>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Dashboard demo

The fallback component is the green rolling icon that displays while loading.

You can find the entire repository here.
Thank you for reading and happy coding!

P.S.: If you have any comments or suggestions please don't hesitate to share below, I'd love to read them.

💖 💪 🙅 🚩
omogbai
Omogbai Atakpu

Posted on July 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related