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:
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;
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();
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;
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;
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;
Now, to implement React Router.
I have added 'Browser Router' to index.tsx here:
...
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
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>
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"));
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>
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>
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;
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;
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.
Top comments (4)
Awesome Job, keep up👍
Thank you.
Anytime 😁👍
You made my day man ❤️✨